@meshxdata/fops 0.1.51 → 0.1.53

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.
Files changed (90) hide show
  1. package/CHANGELOG.md +207 -21
  2. package/package.json +2 -6
  3. package/src/agent/agent.js +6 -0
  4. package/src/commands/setup.js +34 -0
  5. package/src/doctor.js +11 -8
  6. package/src/fleet-registry.js +38 -2
  7. package/src/plugins/__test-fixtures__/fake-plugin.js +2 -0
  8. package/src/plugins/__test-fixtures__/no-register-plugin.js +2 -0
  9. package/src/plugins/__test-fixtures__/with-register/index.js +2 -0
  10. package/src/plugins/__test-fixtures__/without-register/index.js +2 -0
  11. package/src/plugins/api.js +4 -0
  12. package/src/plugins/builtins/docker-compose.js +59 -0
  13. package/src/plugins/bundled/fops-plugin-azure/index.js +4 -0
  14. package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-core.js +53 -53
  15. package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-secrets.js +151 -0
  16. package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-storage.js +2 -2
  17. package/src/plugins/bundled/fops-plugin-azure/lib/azure-cost.js +52 -22
  18. package/src/plugins/bundled/fops-plugin-azure/lib/azure-fleet.js +12 -4
  19. package/src/plugins/bundled/fops-plugin-azure/lib/azure-helpers.js +6 -2
  20. package/src/plugins/bundled/fops-plugin-azure/lib/azure-ops.js +113 -7
  21. package/src/plugins/bundled/fops-plugin-azure/lib/azure-provision-init.js +13 -4
  22. package/src/plugins/bundled/fops-plugin-azure/lib/azure-provision.js +91 -14
  23. package/src/plugins/bundled/fops-plugin-azure/lib/azure-service.js +507 -0
  24. package/src/plugins/bundled/fops-plugin-azure/lib/azure-sync.js +146 -7
  25. package/src/plugins/bundled/fops-plugin-azure/lib/azure.js +1 -1
  26. package/src/plugins/bundled/fops-plugin-azure/lib/commands/test-cmds.js +28 -0
  27. package/src/plugins/bundled/fops-plugin-azure/lib/commands/vm-cmds.js +61 -0
  28. package/src/plugins/bundled/fops-plugin-cloud/api.js +712 -0
  29. package/src/plugins/bundled/fops-plugin-cloud/fops.plugin.json +6 -0
  30. package/src/plugins/bundled/fops-plugin-cloud/index.js +208 -0
  31. package/src/plugins/bundled/fops-plugin-cloud/lib/azure-provider.js +81 -0
  32. package/src/plugins/bundled/fops-plugin-cloud/lib/provider.js +50 -0
  33. package/src/plugins/bundled/fops-plugin-cloud/ui/dist/assets/favicon-C49brna2.svg +15 -0
  34. package/src/plugins/bundled/fops-plugin-cloud/ui/dist/assets/index-CVqQ_kKW.js +65 -0
  35. package/src/plugins/bundled/fops-plugin-cloud/ui/dist/assets/index-DZetahP3.css +1 -0
  36. package/src/plugins/bundled/fops-plugin-cloud/ui/dist/index.html +28 -0
  37. package/src/plugins/bundled/fops-plugin-cloud/ui/index.html +27 -0
  38. package/src/plugins/bundled/fops-plugin-cloud/ui/package-lock.json +2634 -0
  39. package/src/plugins/bundled/fops-plugin-cloud/ui/package.json +29 -0
  40. package/src/plugins/bundled/fops-plugin-cloud/ui/postcss.config.cjs +5 -0
  41. package/src/plugins/bundled/fops-plugin-cloud/ui/src/App.jsx +32 -0
  42. package/src/plugins/bundled/fops-plugin-cloud/ui/src/api/client.js +114 -0
  43. package/src/plugins/bundled/fops-plugin-cloud/ui/src/api/queries.js +111 -0
  44. package/src/plugins/bundled/fops-plugin-cloud/ui/src/components/LogPanel.jsx +162 -0
  45. package/src/plugins/bundled/fops-plugin-cloud/ui/src/components/ThemeToggle.jsx +46 -0
  46. package/src/plugins/bundled/fops-plugin-cloud/ui/src/css/additional-styles/utility-patterns.css +147 -0
  47. package/src/plugins/bundled/fops-plugin-cloud/ui/src/css/style.css +138 -0
  48. package/src/plugins/bundled/fops-plugin-cloud/ui/src/favicon.svg +15 -0
  49. package/src/plugins/bundled/fops-plugin-cloud/ui/src/lib/utils.ts +19 -0
  50. package/src/plugins/bundled/fops-plugin-cloud/ui/src/main.jsx +25 -0
  51. package/src/plugins/bundled/fops-plugin-cloud/ui/src/pages/Audit.jsx +164 -0
  52. package/src/plugins/bundled/fops-plugin-cloud/ui/src/pages/Costs.jsx +305 -0
  53. package/src/plugins/bundled/fops-plugin-cloud/ui/src/pages/CreateResource.jsx +285 -0
  54. package/src/plugins/bundled/fops-plugin-cloud/ui/src/pages/Fleet.jsx +307 -0
  55. package/src/plugins/bundled/fops-plugin-cloud/ui/src/pages/Resources.jsx +229 -0
  56. package/src/plugins/bundled/fops-plugin-cloud/ui/src/partials/Header.jsx +132 -0
  57. package/src/plugins/bundled/fops-plugin-cloud/ui/src/partials/Sidebar.jsx +174 -0
  58. package/src/plugins/bundled/fops-plugin-cloud/ui/src/partials/SidebarLinkGroup.jsx +21 -0
  59. package/src/plugins/bundled/fops-plugin-cloud/ui/src/utils/AuthContext.jsx +170 -0
  60. package/src/plugins/bundled/fops-plugin-cloud/ui/src/utils/Info.jsx +49 -0
  61. package/src/plugins/bundled/fops-plugin-cloud/ui/src/utils/ThemeContext.jsx +37 -0
  62. package/src/plugins/bundled/fops-plugin-cloud/ui/src/utils/Transition.jsx +116 -0
  63. package/src/plugins/bundled/fops-plugin-cloud/ui/src/utils/Utils.js +63 -0
  64. package/src/plugins/bundled/fops-plugin-cloud/ui/vite.config.js +23 -0
  65. package/src/plugins/bundled/fops-plugin-foundation/test-helpers.js +65 -0
  66. package/src/plugins/loader.js +34 -1
  67. package/src/plugins/registry.js +15 -0
  68. package/src/plugins/schemas.js +17 -0
  69. package/src/project.js +1 -1
  70. package/src/serve.js +196 -2
  71. package/src/shell.js +21 -1
  72. package/src/web/admin.html.js +236 -0
  73. package/src/web/api.js +73 -0
  74. package/src/web/dist/assets/index-BphVaAUd.css +1 -0
  75. package/src/web/dist/assets/index-CSckLzuG.js +129 -0
  76. package/src/web/dist/index.html +2 -2
  77. package/src/web/frontend/index.html +16 -0
  78. package/src/web/frontend/src/App.jsx +445 -0
  79. package/src/web/frontend/src/components/ChatView.jsx +910 -0
  80. package/src/web/frontend/src/components/InputBox.jsx +523 -0
  81. package/src/web/frontend/src/components/Sidebar.jsx +410 -0
  82. package/src/web/frontend/src/components/StatusBar.jsx +37 -0
  83. package/src/web/frontend/src/components/TabBar.jsx +87 -0
  84. package/src/web/frontend/src/hooks/useWebSocket.js +412 -0
  85. package/src/web/frontend/src/index.css +78 -0
  86. package/src/web/frontend/src/main.jsx +6 -0
  87. package/src/web/frontend/vite.config.js +21 -0
  88. package/src/web/server.js +64 -1
  89. package/src/web/dist/assets/index-NXC8Hvnp.css +0 -1
  90. package/src/web/dist/assets/index-QH1N4ejK.js +0 -112
@@ -0,0 +1,132 @@
1
+ import React, { useState, useRef, useEffect } from "react";
2
+ import { useAuth0 } from "@auth0/auth0-react";
3
+ import ThemeToggle from "../components/ThemeToggle";
4
+ import { useAuthContext } from "../utils/AuthContext";
5
+
6
+ function UserMenu() {
7
+ const { logout } = useAuth0();
8
+ const { user, roles, isAdmin, canWrite } = useAuthContext();
9
+ const [open, setOpen] = useState(false);
10
+ const ref = useRef(null);
11
+
12
+ useEffect(() => {
13
+ const handler = (e) => {
14
+ if (ref.current && !ref.current.contains(e.target)) setOpen(false);
15
+ };
16
+ document.addEventListener("mousedown", handler);
17
+ return () => document.removeEventListener("mousedown", handler);
18
+ }, []);
19
+
20
+ const initials = (user?.name || user?.email || "?")
21
+ .split(/[\s@]+/)
22
+ .slice(0, 2)
23
+ .map((s) => s[0]?.toUpperCase())
24
+ .join("");
25
+
26
+ const roleBadge = isAdmin ? "Admin" : canWrite ? "Operator" : "Viewer";
27
+ const badgeColor = isAdmin
28
+ ? "bg-violet-100 text-violet-700 dark:bg-violet-500/20 dark:text-violet-300"
29
+ : canWrite
30
+ ? "bg-sky-100 text-sky-700 dark:bg-sky-500/20 dark:text-sky-300"
31
+ : "bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300";
32
+
33
+ return (
34
+ <div className="relative" ref={ref}>
35
+ <button
36
+ onClick={() => setOpen(!open)}
37
+ className="flex items-center space-x-2 text-sm focus:outline-none"
38
+ >
39
+ {user?.picture ? (
40
+ <img src={user.picture} alt="" className="w-8 h-8 rounded-full" />
41
+ ) : (
42
+ <div className="w-8 h-8 rounded-full bg-violet-500 text-white flex items-center justify-center text-xs font-bold">
43
+ {initials}
44
+ </div>
45
+ )}
46
+ <span className="hidden sm:block text-gray-700 dark:text-gray-200 font-medium truncate max-w-[120px]">
47
+ {user?.name || user?.email}
48
+ </span>
49
+ <svg className="w-3 h-3 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
50
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
51
+ </svg>
52
+ </button>
53
+
54
+ {open && (
55
+ <div className="absolute right-0 mt-2 w-64 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 py-2 z-50">
56
+ <div className="px-4 py-2 border-b border-gray-100 dark:border-gray-700">
57
+ <p className="text-sm font-medium text-gray-800 dark:text-gray-100 truncate">
58
+ {user?.name || user?.email}
59
+ </p>
60
+ <p className="text-xs text-gray-500 dark:text-gray-400 truncate">{user?.email}</p>
61
+ <div className="mt-1.5 flex flex-wrap gap-1">
62
+ <span className={`inline-block text-[10px] font-semibold px-1.5 py-0.5 rounded ${badgeColor}`}>
63
+ {roleBadge}
64
+ </span>
65
+ {roles.filter((r) => r !== "admin").map((r) => (
66
+ <span key={r} className="inline-block text-[10px] font-medium px-1.5 py-0.5 rounded bg-gray-100 text-gray-500 dark:bg-gray-700 dark:text-gray-400">
67
+ {r}
68
+ </span>
69
+ ))}
70
+ </div>
71
+ </div>
72
+ <button
73
+ onClick={() => logout({ logoutParams: { returnTo: window.location.origin + "/cloud" } })}
74
+ className="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700/50"
75
+ >
76
+ Sign out
77
+ </button>
78
+ </div>
79
+ )}
80
+ </div>
81
+ );
82
+ }
83
+
84
+ function Header({ sidebarOpen, setSidebarOpen, variant = "default" }) {
85
+ const { user } = useAuthContext();
86
+ return (
87
+ <header
88
+ className={`sticky top-0 before:absolute before:inset-0 before:backdrop-blur-md max-lg:before:bg-white/90 dark:max-lg:before:bg-gray-800/90 before:-z-10 z-30 ${variant === "v2" ? "before:bg-white after:absolute after:h-px after:inset-x-0 after:top-full after:bg-gray-200 dark:after:bg-gray-700/60 after:-z-10 dark:before:bg-gray-800" : "max-lg:shadow-xs lg:before:bg-gray-100/90 dark:lg:before:bg-gray-900/90"}`}
89
+ >
90
+ <div className="px-4 sm:px-6 lg:px-8">
91
+ <div className={`flex items-center justify-between h-16 ${variant !== "v2" ? "lg:border-b border-gray-200 dark:border-gray-700/60" : ""}`}>
92
+ {/* Left side */}
93
+ <div className="flex">
94
+ <button
95
+ className="text-gray-500 hover:text-gray-600 dark:hover:text-gray-400 lg:hidden"
96
+ aria-controls="sidebar"
97
+ aria-expanded={sidebarOpen}
98
+ onClick={(e) => {
99
+ e.stopPropagation();
100
+ setSidebarOpen(!sidebarOpen);
101
+ }}
102
+ >
103
+ <span className="sr-only">Open sidebar</span>
104
+ <svg className="w-6 h-6 fill-current" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
105
+ <rect x="4" y="5" width="16" height="2" />
106
+ <rect x="4" y="11" width="16" height="2" />
107
+ <rect x="4" y="17" width="16" height="2" />
108
+ </svg>
109
+ </button>
110
+ </div>
111
+
112
+ {/* Right side */}
113
+ <div className="flex items-center space-x-3">
114
+ <ThemeToggle />
115
+ <hr className="w-px h-6 bg-gray-200 dark:bg-gray-700/60 border-none" />
116
+ {user?.sub !== "local" && (
117
+ <>
118
+ <UserMenu />
119
+ <hr className="w-px h-6 bg-gray-200 dark:bg-gray-700/60 border-none" />
120
+ </>
121
+ )}
122
+ <a href="/" className="text-xs text-gray-500 hover:text-gray-600 dark:hover:text-gray-400 font-medium">
123
+ &larr; fops
124
+ </a>
125
+ </div>
126
+ </div>
127
+ </div>
128
+ </header>
129
+ );
130
+ }
131
+
132
+ export default Header;
@@ -0,0 +1,174 @@
1
+ import React, { useState, useEffect, useRef } from "react";
2
+ import { NavLink, useLocation } from "react-router-dom";
3
+
4
+ function Sidebar({ sidebarOpen, setSidebarOpen }) {
5
+ const location = useLocation();
6
+ const { pathname } = location;
7
+
8
+ const trigger = useRef(null);
9
+ const sidebar = useRef(null);
10
+
11
+ const storedSidebarExpanded = localStorage.getItem("sidebar-expanded");
12
+ const [sidebarExpanded, setSidebarExpanded] = useState(
13
+ storedSidebarExpanded === null ? false : storedSidebarExpanded === "true",
14
+ );
15
+
16
+ useEffect(() => {
17
+ const clickHandler = ({ target }) => {
18
+ if (!sidebar.current || !trigger.current) return;
19
+ if (!sidebarOpen || sidebar.current.contains(target) || trigger.current.contains(target)) return;
20
+ setSidebarOpen(false);
21
+ };
22
+ document.addEventListener("click", clickHandler);
23
+ return () => document.removeEventListener("click", clickHandler);
24
+ });
25
+
26
+ useEffect(() => {
27
+ const keyHandler = ({ keyCode }) => {
28
+ if (!sidebarOpen || keyCode !== 27) return;
29
+ setSidebarOpen(false);
30
+ };
31
+ document.addEventListener("keydown", keyHandler);
32
+ return () => document.removeEventListener("keydown", keyHandler);
33
+ });
34
+
35
+ useEffect(() => {
36
+ localStorage.setItem("sidebar-expanded", sidebarExpanded);
37
+ if (sidebarExpanded) {
38
+ document.querySelector("body").classList.add("sidebar-expanded");
39
+ } else {
40
+ document.querySelector("body").classList.remove("sidebar-expanded");
41
+ }
42
+ }, [sidebarExpanded]);
43
+
44
+ const navItems = [
45
+ {
46
+ to: "/",
47
+ label: "Registry",
48
+ icon: (
49
+ <svg className="shrink-0 fill-current" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
50
+ <path d="M3 1h10a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2Zm0 2v10h10V3H3Zm2 2h6v2H5V5Zm0 4h6v2H5V9Z" />
51
+ </svg>
52
+ ),
53
+ },
54
+ {
55
+ to: "/fleet",
56
+ label: "Fleet",
57
+ icon: (
58
+ <svg className="shrink-0 fill-current" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
59
+ <path d="M8 0a1 1 0 0 1 1 1v2.1A5.002 5.002 0 0 1 13 8a5 5 0 0 1-5 5 5 5 0 0 1-5-5 5.002 5.002 0 0 1 4-4.9V1a1 1 0 0 1 1-1ZM6 8a2 2 0 1 0 4 0 2 2 0 0 0-4 0Z" />
60
+ </svg>
61
+ ),
62
+ },
63
+ {
64
+ to: "/costs",
65
+ label: "Costs",
66
+ icon: (
67
+ <svg className="shrink-0 fill-current" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
68
+ <path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8Zm0 14c-3.3 0-6-2.7-6-6s2.7-6 6-6 6 2.7 6 6-2.7 6-6 6Z" />
69
+ <path d="M9 4H7v4.4l3.2 2.4.8-1.2L9 8V4Z" />
70
+ </svg>
71
+ ),
72
+ },
73
+ {
74
+ to: "/audit",
75
+ label: "Audit",
76
+ icon: (
77
+ <svg className="shrink-0 fill-current" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
78
+ <path d="M8 0L1 3v5c0 4.1 3 7.4 7 8 4-0.6 7-3.9 7-8V3L8 0Zm0 2.2l5 2.1v4.2c0 3-2.2 5.6-5 6.2-2.8-0.6-5-3.2-5-6.2V4.3l5-2.1Zm2.3 4L7 9.5 5.7 8.2l-1 1L7 11.5l4.3-4.3-1-1Z" />
79
+ </svg>
80
+ ),
81
+ },
82
+ ];
83
+
84
+ return (
85
+ <div className="min-w-fit">
86
+ {/* Sidebar backdrop (mobile only) */}
87
+ <div
88
+ className={`fixed inset-0 bg-gray-900/30 z-40 lg:hidden lg:z-auto transition-opacity duration-200 ${sidebarOpen ? "opacity-100" : "opacity-0 pointer-events-none"}`}
89
+ aria-hidden="true"
90
+ />
91
+
92
+ {/* Sidebar */}
93
+ <div
94
+ id="sidebar"
95
+ ref={sidebar}
96
+ className={`flex lg:flex! flex-col absolute z-40 left-0 top-0 lg:static lg:left-auto lg:top-auto lg:translate-x-0 h-[100dvh] overflow-y-scroll lg:overflow-y-auto no-scrollbar w-64 lg:w-20 lg:sidebar-expanded:!w-64 2xl:w-64! shrink-0 bg-white dark:bg-gray-800 p-4 transition-all duration-200 ease-in-out ${sidebarOpen ? "translate-x-0" : "-translate-x-64"} border-r border-gray-200 dark:border-gray-700/60`}
97
+ >
98
+ {/* Sidebar header */}
99
+ <div className="flex justify-between mb-10 pr-3 sm:px-2">
100
+ <button
101
+ ref={trigger}
102
+ className="lg:hidden text-gray-500 hover:text-gray-400"
103
+ onClick={() => setSidebarOpen(!sidebarOpen)}
104
+ aria-controls="sidebar"
105
+ aria-expanded={sidebarOpen}
106
+ >
107
+ <span className="sr-only">Close sidebar</span>
108
+ <svg className="w-6 h-6 fill-current" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
109
+ <path d="M10.7 18.7l1.4-1.4L7.8 13H20v-2H7.8l4.3-4.3-1.4-1.4L4 12z" />
110
+ </svg>
111
+ </button>
112
+ {/* Logo */}
113
+ <NavLink end to="/" className="block">
114
+ <svg className="fill-violet-500" xmlns="http://www.w3.org/2000/svg" width={32} height={32} viewBox="0 0 32 32">
115
+ <path d="M16 2a14 14 0 1 0 14 14A14 14 0 0 0 16 2Zm0 26a12 12 0 1 1 12-12 12 12 0 0 1-12 12Z" />
116
+ <path d="M16 8a8 8 0 1 0 8 8 8 8 0 0 0-8-8Zm0 14a6 6 0 1 1 6-6 6 6 0 0 1-6 6Z" />
117
+ <circle cx="16" cy="16" r="3" />
118
+ </svg>
119
+ </NavLink>
120
+ </div>
121
+
122
+ {/* Links */}
123
+ <div className="space-y-8">
124
+ <div>
125
+ <h3 className="text-xs uppercase text-gray-400 dark:text-gray-500 font-semibold pl-3">
126
+ <span className="hidden lg:block lg:sidebar-expanded:hidden 2xl:hidden text-center w-6" aria-hidden="true">
127
+ &bull;&bull;&bull;
128
+ </span>
129
+ <span className="lg:hidden lg:sidebar-expanded:block 2xl:block">Cloud</span>
130
+ </h3>
131
+ <ul className="mt-3">
132
+ {navItems.map((item) => (
133
+ <li key={item.to} className="pl-4 pr-3 py-2 rounded-lg mb-0.5 last:mb-0">
134
+ <NavLink
135
+ end={item.to === "/"}
136
+ to={item.to}
137
+ className={({ isActive }) =>
138
+ `block text-gray-800 dark:text-gray-100 truncate transition ${isActive ? "" : "hover:text-gray-900 dark:hover:text-white"}`
139
+ }
140
+ >
141
+ {({ isActive }) => (
142
+ <div className="flex items-center">
143
+ <div className={isActive ? "text-violet-500" : "text-gray-400 dark:text-gray-500"}>
144
+ {item.icon}
145
+ </div>
146
+ <span className={`text-sm font-medium ml-4 lg:opacity-0 lg:sidebar-expanded:opacity-100 2xl:opacity-100 duration-200 ${isActive ? "text-violet-500" : ""}`}>
147
+ {item.label}
148
+ </span>
149
+ </div>
150
+ )}
151
+ </NavLink>
152
+ </li>
153
+ ))}
154
+ </ul>
155
+ </div>
156
+ </div>
157
+
158
+ {/* Expand / collapse button */}
159
+ <div className="pt-3 hidden lg:inline-flex 2xl:hidden justify-end mt-auto">
160
+ <div className="w-12 pl-4 pr-3 py-2">
161
+ <button className="text-gray-400 hover:text-gray-500 dark:text-gray-500 dark:hover:text-gray-400" onClick={() => setSidebarExpanded(!sidebarExpanded)}>
162
+ <span className="sr-only">Expand / collapse sidebar</span>
163
+ <svg className={`shrink-0 fill-current text-gray-400 dark:text-gray-600 sidebar-expanded:rotate-180`} xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
164
+ <path d="M6.7 14.7l1.4-1.4L3.8 9H16V7H3.8l4.3-4.3-1.4-1.4L0 8z" />
165
+ </svg>
166
+ </button>
167
+ </div>
168
+ </div>
169
+ </div>
170
+ </div>
171
+ );
172
+ }
173
+
174
+ export default Sidebar;
@@ -0,0 +1,21 @@
1
+ import React, { useState } from 'react';
2
+
3
+ function SidebarLinkGroup({
4
+ children,
5
+ activecondition,
6
+ }) {
7
+
8
+ const [open, setOpen] = useState(activecondition);
9
+
10
+ const handleClick = () => {
11
+ setOpen(!open);
12
+ }
13
+
14
+ return (
15
+ <li className={`pl-4 pr-3 py-2 rounded-lg mb-0.5 last:mb-0 bg-linear-to-r ${activecondition && 'from-violet-500/[0.12] dark:from-violet-500/[0.24] to-violet-500/[0.04]'}`}>
16
+ {children(handleClick, open)}
17
+ </li>
18
+ );
19
+ }
20
+
21
+ export default SidebarLinkGroup;
@@ -0,0 +1,170 @@
1
+ import { createContext, useContext, useEffect, useState } from "react";
2
+ import { Auth0Provider, useAuth0 } from "@auth0/auth0-react";
3
+ import { setTokenGetter } from "../api/client";
4
+
5
+ const AuthContext = createContext({
6
+ user: null,
7
+ roles: [],
8
+ permissions: [],
9
+ isAdmin: false,
10
+ canWrite: false,
11
+ canDeploy: false,
12
+ getToken: async () => "",
13
+ });
14
+
15
+ /**
16
+ * Inner provider that fetches user profile + roles from the API
17
+ * once authenticated.
18
+ */
19
+ function AuthGate({ children }) {
20
+ const { isAuthenticated, isLoading, loginWithRedirect, getAccessTokenSilently, user } = useAuth0();
21
+ const [profile, setProfile] = useState(null);
22
+ const [loadingProfile, setLoadingProfile] = useState(true);
23
+
24
+ // Wire token getter into the API client so all fetches are authenticated
25
+ useEffect(() => {
26
+ if (isAuthenticated) setTokenGetter(() => getAccessTokenSilently());
27
+ }, [isAuthenticated, getAccessTokenSilently]);
28
+
29
+ useEffect(() => {
30
+ if (!isAuthenticated) return;
31
+ let cancelled = false;
32
+
33
+ (async () => {
34
+ try {
35
+ const token = await getAccessTokenSilently();
36
+ const res = await fetch("/cloud/api/me", {
37
+ headers: { Authorization: `Bearer ${token}` },
38
+ });
39
+ if (res.ok && !cancelled) {
40
+ setProfile(await res.json());
41
+ }
42
+ } catch {
43
+ // token or fetch failed — will show login
44
+ } finally {
45
+ if (!cancelled) setLoadingProfile(false);
46
+ }
47
+ })();
48
+
49
+ return () => { cancelled = true; };
50
+ }, [isAuthenticated, getAccessTokenSilently]);
51
+
52
+ if (isLoading) {
53
+ return (
54
+ <div className="flex items-center justify-center h-screen bg-gray-100 dark:bg-gray-900">
55
+ <div className="text-gray-500 dark:text-gray-400 text-sm">Loading...</div>
56
+ </div>
57
+ );
58
+ }
59
+
60
+ if (!isAuthenticated) {
61
+ return (
62
+ <div className="flex items-center justify-center h-screen bg-gray-100 dark:bg-gray-900">
63
+ <div className="text-center">
64
+ <h1 className="text-2xl font-bold text-gray-800 dark:text-gray-100 mb-2">meshXcloud</h1>
65
+ <p className="text-gray-500 dark:text-gray-400 mb-6">Sign in to access the cloud panel</p>
66
+ <button
67
+ onClick={() => loginWithRedirect()}
68
+ className="px-6 py-2.5 bg-violet-500 hover:bg-violet-600 text-white rounded-lg font-medium transition-colors"
69
+ >
70
+ Sign in with Auth0
71
+ </button>
72
+ </div>
73
+ </div>
74
+ );
75
+ }
76
+
77
+ if (loadingProfile) {
78
+ return (
79
+ <div className="flex items-center justify-center h-screen bg-gray-100 dark:bg-gray-900">
80
+ <div className="text-gray-500 dark:text-gray-400 text-sm">Loading profile...</div>
81
+ </div>
82
+ );
83
+ }
84
+
85
+ const roles = profile?.roles || [];
86
+ const permissions = profile?.permissions || [];
87
+ const isAdmin = roles.includes("admin");
88
+
89
+ const value = {
90
+ user: { ...user, ...profile },
91
+ roles,
92
+ permissions,
93
+ isAdmin,
94
+ canWrite: isAdmin || permissions.includes("cloud:write") || permissions.includes("cloud:admin"),
95
+ canDeploy: isAdmin || permissions.includes("cloud:deploy") || permissions.includes("cloud:admin"),
96
+ getToken: getAccessTokenSilently,
97
+ };
98
+
99
+ return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
100
+ }
101
+
102
+ /**
103
+ * Passthrough provider when auth is disabled — grants full admin access.
104
+ */
105
+ function NoAuthProvider({ children }) {
106
+ const value = {
107
+ user: { sub: "local", name: "Local User", email: "" },
108
+ roles: ["admin"],
109
+ permissions: ["cloud:admin"],
110
+ isAdmin: true,
111
+ canWrite: true,
112
+ canDeploy: true,
113
+ getToken: async () => "",
114
+ };
115
+ return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
116
+ }
117
+
118
+ /**
119
+ * Top-level wrapper that fetches Auth0 config from the API.
120
+ * If auth is disabled (CLOUD_AUTH != 1), renders children directly
121
+ * with full permissions. Otherwise, wraps in Auth0Provider + AuthGate.
122
+ */
123
+ export default function AuthProvider({ children }) {
124
+ const [config, setConfig] = useState(null);
125
+ const [error, setError] = useState(null);
126
+
127
+ useEffect(() => {
128
+ fetch("/cloud/api/auth-config")
129
+ .then((r) => r.json())
130
+ .then(setConfig)
131
+ .catch((e) => setError(e.message));
132
+ }, []);
133
+
134
+ if (error) {
135
+ return (
136
+ <div className="flex items-center justify-center h-screen bg-gray-100 dark:bg-gray-900">
137
+ <div className="text-red-500 text-sm">Auth config error: {error}</div>
138
+ </div>
139
+ );
140
+ }
141
+
142
+ if (!config) {
143
+ return (
144
+ <div className="flex items-center justify-center h-screen bg-gray-100 dark:bg-gray-900">
145
+ <div className="text-gray-500 dark:text-gray-400 text-sm">Initializing...</div>
146
+ </div>
147
+ );
148
+ }
149
+
150
+ // Auth disabled — bypass Auth0 entirely
151
+ if (!config.enabled) {
152
+ return <NoAuthProvider>{children}</NoAuthProvider>;
153
+ }
154
+
155
+ return (
156
+ <Auth0Provider
157
+ domain={config.domain}
158
+ clientId={config.clientId}
159
+ authorizationParams={{
160
+ redirect_uri: window.location.origin + "/cloud",
161
+ audience: config.audience,
162
+ }}
163
+ cacheLocation="localstorage"
164
+ >
165
+ <AuthGate>{children}</AuthGate>
166
+ </Auth0Provider>
167
+ );
168
+ }
169
+
170
+ export const useAuthContext = () => useContext(AuthContext);
@@ -0,0 +1,49 @@
1
+ import React, { useState } from 'react';
2
+ import Transition from './Transition';
3
+
4
+ function Info({
5
+ children,
6
+ className,
7
+ containerClassName
8
+ }) {
9
+
10
+ const [infoOpen, setInfoOpen] = useState(false);
11
+
12
+ return (
13
+ <div
14
+ className={`relative ${className}`}
15
+ onMouseEnter={() => setInfoOpen(true)}
16
+ onMouseLeave={() => setInfoOpen(false)}
17
+ onFocus={() => setInfoOpen(true)}
18
+ onBlur={() => setInfoOpen(false)}
19
+ >
20
+ <button
21
+ className="block"
22
+ aria-haspopup="true"
23
+ aria-expanded={infoOpen}
24
+ onClick={(e) => e.preventDefault()}
25
+ >
26
+ <svg className="w-4 h-4 fill-current text-gray-400" viewBox="0 0 16 16">
27
+ <path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm0 12c-.6 0-1-.4-1-1s.4-1 1-1 1 .4 1 1-.4 1-1 1zm1-3H7V4h2v5z" />
28
+ </svg>
29
+ </button>
30
+ <div className="z-10 absolute bottom-full left-1/2 transform -translate-x-1/2">
31
+ <Transition
32
+ show={infoOpen}
33
+ tag="div"
34
+ className={`bg-white border border-gray-200 p-3 rounded-sm shadow-lg overflow-hidden mb-2 ${containerClassName}`}
35
+ enter="transition ease-out duration-200 transform"
36
+ enterStart="opacity-0 -translate-y-2"
37
+ enterEnd="opacity-100 translate-y-0"
38
+ leave="transition ease-out duration-200"
39
+ leaveStart="opacity-100"
40
+ leaveEnd="opacity-0"
41
+ >
42
+ {children}
43
+ </Transition>
44
+ </div>
45
+ </div>
46
+ );
47
+ }
48
+
49
+ export default Info;
@@ -0,0 +1,37 @@
1
+ import { createContext, useContext, useState, useEffect } from 'react';
2
+
3
+ const ThemeContext = createContext({
4
+ currentTheme: 'light',
5
+ changeCurrentTheme: () => {},
6
+ });
7
+
8
+ export default function ThemeProvider({children}) {
9
+ const persistedTheme = localStorage.getItem('theme');
10
+ const [theme, setTheme] = useState(persistedTheme || 'light');
11
+
12
+ const changeCurrentTheme = (newTheme) => {
13
+ setTheme(newTheme);
14
+ localStorage.setItem('theme', newTheme);
15
+ };
16
+
17
+ useEffect(() => {
18
+ document.documentElement.classList.add('**:transition-none!');
19
+ if (theme === 'light') {
20
+ document.documentElement.classList.remove('dark');
21
+ document.documentElement.style.colorScheme = 'light';
22
+ } else {
23
+ document.documentElement.classList.add('dark');
24
+ document.documentElement.style.colorScheme = 'dark';
25
+ }
26
+
27
+ const transitionTimeout = setTimeout(() => {
28
+ document.documentElement.classList.remove('**:transition-none!');
29
+ }, 1);
30
+
31
+ return () => clearTimeout(transitionTimeout);
32
+ }, [theme]);
33
+
34
+ return <ThemeContext.Provider value={{ currentTheme: theme, changeCurrentTheme }}>{children}</ThemeContext.Provider>;
35
+ }
36
+
37
+ export const useThemeProvider = () => useContext(ThemeContext);