@meshxdata/fops 0.1.54 → 0.1.57

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 (34) hide show
  1. package/CHANGELOG.md +184 -1
  2. package/package.json +1 -2
  3. package/src/commands/index.js +2 -0
  4. package/src/commands/k3s-cmd.js +124 -0
  5. package/src/commands/lifecycle.js +7 -0
  6. package/src/plugins/builtins/docker-compose.js +7 -13
  7. package/src/plugins/bundled/fops-plugin-azure/lib/azure-openai.js +0 -3
  8. package/src/plugins/bundled/fops-plugin-azure/lib/commands/vm-cmds.js +5 -2
  9. package/src/plugins/bundled/fops-plugin-cloud/api.js +14 -0
  10. package/src/project.js +12 -7
  11. package/src/plugins/bundled/fops-plugin-cloud/ui/postcss.config.cjs +0 -5
  12. package/src/plugins/bundled/fops-plugin-cloud/ui/src/App.jsx +0 -32
  13. package/src/plugins/bundled/fops-plugin-cloud/ui/src/api/client.js +0 -114
  14. package/src/plugins/bundled/fops-plugin-cloud/ui/src/api/queries.js +0 -111
  15. package/src/plugins/bundled/fops-plugin-cloud/ui/src/components/LogPanel.jsx +0 -162
  16. package/src/plugins/bundled/fops-plugin-cloud/ui/src/components/ThemeToggle.jsx +0 -46
  17. package/src/plugins/bundled/fops-plugin-cloud/ui/src/css/additional-styles/utility-patterns.css +0 -147
  18. package/src/plugins/bundled/fops-plugin-cloud/ui/src/css/style.css +0 -138
  19. package/src/plugins/bundled/fops-plugin-cloud/ui/src/favicon.svg +0 -15
  20. package/src/plugins/bundled/fops-plugin-cloud/ui/src/lib/utils.ts +0 -19
  21. package/src/plugins/bundled/fops-plugin-cloud/ui/src/main.jsx +0 -25
  22. package/src/plugins/bundled/fops-plugin-cloud/ui/src/pages/Audit.jsx +0 -164
  23. package/src/plugins/bundled/fops-plugin-cloud/ui/src/pages/Costs.jsx +0 -305
  24. package/src/plugins/bundled/fops-plugin-cloud/ui/src/pages/CreateResource.jsx +0 -285
  25. package/src/plugins/bundled/fops-plugin-cloud/ui/src/pages/Fleet.jsx +0 -307
  26. package/src/plugins/bundled/fops-plugin-cloud/ui/src/pages/Resources.jsx +0 -229
  27. package/src/plugins/bundled/fops-plugin-cloud/ui/src/partials/Header.jsx +0 -132
  28. package/src/plugins/bundled/fops-plugin-cloud/ui/src/partials/Sidebar.jsx +0 -174
  29. package/src/plugins/bundled/fops-plugin-cloud/ui/src/partials/SidebarLinkGroup.jsx +0 -21
  30. package/src/plugins/bundled/fops-plugin-cloud/ui/src/utils/AuthContext.jsx +0 -170
  31. package/src/plugins/bundled/fops-plugin-cloud/ui/src/utils/Info.jsx +0 -49
  32. package/src/plugins/bundled/fops-plugin-cloud/ui/src/utils/ThemeContext.jsx +0 -37
  33. package/src/plugins/bundled/fops-plugin-cloud/ui/src/utils/Transition.jsx +0 -116
  34. package/src/plugins/bundled/fops-plugin-cloud/ui/src/utils/Utils.js +0 -63
@@ -1,229 +0,0 @@
1
- import React, { useState, useCallback } from "react";
2
- import { useNavigate } from "react-router-dom";
3
- import Sidebar from "../partials/Sidebar";
4
- import Header from "../partials/Header";
5
- import LogPanel from "../components/LogPanel";
6
- import { useResources, useResourceAction, useDeleteResource } from "../api/queries";
7
- import { useAuthContext } from "../utils/AuthContext";
8
-
9
- function Resources() {
10
- const [sidebarOpen, setSidebarOpen] = useState(false);
11
- const [actionLogs, setActionLogs] = useState([]);
12
- const navigate = useNavigate();
13
- const { data, isLoading } = useResources();
14
- const action = useResourceAction();
15
- const deleteRes = useDeleteResource();
16
- const [deleteTarget, setDeleteTarget] = useState(null);
17
- const { canWrite } = useAuthContext();
18
- const onActionLine = useCallback((text, type) => setActionLogs((p) => [...p, { text, type }]), []);
19
-
20
- const vms = data?.vms || [];
21
- const clusters = data?.clusters || [];
22
-
23
- const thClass = "px-5 py-3 font-semibold text-left whitespace-nowrap";
24
-
25
- return (
26
- <div className="flex h-screen overflow-hidden">
27
- <Sidebar sidebarOpen={sidebarOpen} setSidebarOpen={setSidebarOpen} />
28
- <div className="relative flex flex-col flex-1 overflow-y-auto overflow-x-hidden">
29
- <Header sidebarOpen={sidebarOpen} setSidebarOpen={setSidebarOpen} />
30
- <main className="grow">
31
- <div className="px-4 sm:px-6 lg:px-8 py-8 w-full max-w-9xl mx-auto">
32
- {/* Page header */}
33
- <div className="sm:flex sm:justify-between sm:items-center mb-8">
34
- <div className="mb-4 sm:mb-0">
35
- <h1 className="text-2xl md:text-3xl text-gray-800 dark:text-gray-100 font-bold">Registry</h1>
36
- </div>
37
- {canWrite && (
38
- <div className="grid grid-flow-col sm:auto-cols-max justify-start sm:justify-end gap-2">
39
- <button className="btn bg-gray-900 text-gray-100 hover:bg-gray-800 dark:bg-gray-100 dark:text-gray-800 dark:hover:bg-white" onClick={() => navigate("/resources/new")}>
40
- <span>New Resource</span>
41
- </button>
42
- </div>
43
- )}
44
- </div>
45
-
46
- {isLoading ? (
47
- <div className="bg-white dark:bg-gray-800 shadow-xs rounded-xl text-sm text-gray-400 dark:text-gray-500 text-center py-12">Loading resources...</div>
48
- ) : (
49
- <div className="space-y-6">
50
- {/* Virtual Machines */}
51
- <div className="bg-white dark:bg-gray-800 shadow-xs rounded-xl">
52
- <header className="px-5 py-4 border-b border-gray-100 dark:border-gray-700/60">
53
- <h2 className="font-semibold text-gray-800 dark:text-gray-100">Virtual Machines <span className="text-gray-400 dark:text-gray-500 font-normal text-sm ml-1">({vms.length})</span></h2>
54
- </header>
55
- <div className="overflow-x-auto">
56
- <table className="table-auto w-full dark:text-gray-300">
57
- <thead className="text-xs uppercase text-gray-400 dark:text-gray-500 bg-gray-50 dark:bg-gray-900/20 border-t border-b border-gray-100 dark:border-gray-700/60">
58
- <tr>
59
- <th className={thClass}>Name</th>
60
- <th className={thClass}>Location</th>
61
- <th className={thClass}>Resource Group</th>
62
- <th className={thClass}>IP</th>
63
- <th className={thClass}>Status</th>
64
- <th className="px-5 py-3 font-semibold text-right whitespace-nowrap">Actions</th>
65
- </tr>
66
- </thead>
67
- <tbody className="text-sm divide-y divide-gray-100 dark:divide-gray-700/60">
68
- {vms.length === 0 ? (
69
- <tr><td colSpan="6" className="px-5 py-8 text-center text-gray-400 dark:text-gray-500">No VMs found</td></tr>
70
- ) : vms.map((vm) => (
71
- <tr key={vm.name}>
72
- <td className="px-5 py-3 whitespace-nowrap">
73
- <div className="font-medium text-gray-800 dark:text-gray-100">{vm.name}</div>
74
- </td>
75
- <td className="px-5 py-3 whitespace-nowrap">{vm.location || "—"}</td>
76
- <td className="px-5 py-3 whitespace-nowrap font-mono text-xs">{vm.resourceGroup || "—"}</td>
77
- <td className="px-5 py-3 whitespace-nowrap font-mono text-xs">{vm.publicIp || "—"}</td>
78
- <td className="px-5 py-3 whitespace-nowrap">
79
- <span className={`text-xs font-medium px-2 py-0.5 rounded-full ${vm.publicIp ? "bg-green-500/20 text-green-700 dark:text-green-400" : "bg-red-500/20 text-red-700 dark:text-red-400"}`}>
80
- {vm.publicIp ? "running" : "stopped"}
81
- </span>
82
- </td>
83
- <td className="px-5 py-3 whitespace-nowrap text-right">
84
- <div className="flex items-center justify-end gap-1">
85
- {vm.publicUrl && (
86
- <a href={vm.publicUrl} target="_blank" rel="noreferrer" className="btn-xs bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700/60 text-violet-500 rounded-lg shadow-xs hover:border-gray-300 dark:hover:border-gray-600">Open</a>
87
- )}
88
- {canWrite && (
89
- <>
90
- <button className="btn-xs bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700/60 text-gray-500 rounded-lg shadow-xs hover:border-gray-300 dark:hover:border-gray-600" onClick={() => { setActionLogs([]); action.mutate({ type: "vm", name: vm.name, action: "start", onLine: onActionLine }); }} disabled={action.isPending}>Start</button>
91
- <button className="btn-xs bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700/60 text-gray-500 rounded-lg shadow-xs hover:border-gray-300 dark:hover:border-gray-600" onClick={() => { setActionLogs([]); action.mutate({ type: "vm", name: vm.name, action: "stop", onLine: onActionLine }); }} disabled={action.isPending}>Stop</button>
92
- <button className="btn-xs bg-red-500/10 border-transparent text-red-500 rounded-lg shadow-xs hover:bg-red-500/20" onClick={() => setDeleteTarget({ type: "vm", name: vm.name })}>Delete</button>
93
- </>
94
- )}
95
- </div>
96
- </td>
97
- </tr>
98
- ))}
99
- </tbody>
100
- </table>
101
- </div>
102
- </div>
103
-
104
- {/* AKS Clusters */}
105
- <div className="bg-white dark:bg-gray-800 shadow-xs rounded-xl">
106
- <header className="px-5 py-4 border-b border-gray-100 dark:border-gray-700/60">
107
- <h2 className="font-semibold text-gray-800 dark:text-gray-100">AKS Clusters <span className="text-gray-400 dark:text-gray-500 font-normal text-sm ml-1">({clusters.length})</span></h2>
108
- </header>
109
- <div className="overflow-x-auto">
110
- <table className="table-auto w-full dark:text-gray-300">
111
- <thead className="text-xs uppercase text-gray-400 dark:text-gray-500 bg-gray-50 dark:bg-gray-900/20 border-t border-b border-gray-100 dark:border-gray-700/60">
112
- <tr>
113
- <th className={thClass}>Name</th>
114
- <th className={thClass}>Location</th>
115
- <th className={thClass}>K8s</th>
116
- <th className={thClass}>HA Pairing</th>
117
- <th className={thClass}>Storage</th>
118
- <th className={thClass}>Key Vault</th>
119
- <th className={thClass}>Postgres</th>
120
- <th className="px-5 py-3 font-semibold text-right whitespace-nowrap">Actions</th>
121
- </tr>
122
- </thead>
123
- <tbody className="text-sm divide-y divide-gray-100 dark:divide-gray-700/60">
124
- {clusters.length === 0 ? (
125
- <tr><td colSpan="8" className="px-5 py-8 text-center text-gray-400 dark:text-gray-500">No clusters found</td></tr>
126
- ) : clusters.map((c) => {
127
- const standbyName = c.ha?.standbyCluster;
128
- const primaryName = c.isStandby ? c.primaryCluster : null;
129
- return (
130
- <tr key={c.name}>
131
- <td className="px-5 py-3 whitespace-nowrap">
132
- <div className="flex items-center gap-2">
133
- <span className="font-medium text-gray-800 dark:text-gray-100">{c.name}</span>
134
- {c.active && <span className="text-[10px] font-medium px-1.5 py-0.5 rounded-full bg-violet-500/20 text-violet-600 dark:text-violet-400">active</span>}
135
- {c.isStandby && <span className="text-[10px] font-medium px-1.5 py-0.5 rounded-full bg-yellow-500/20 text-yellow-600 dark:text-yellow-400">standby</span>}
136
- </div>
137
- {c.domain && <div className="text-xs text-gray-400 dark:text-gray-500">{c.domain}</div>}
138
- </td>
139
- <td className="px-5 py-3 whitespace-nowrap">{c.location || "—"}</td>
140
- <td className="px-5 py-3 whitespace-nowrap font-mono text-xs">{c.kubernetesVersion || "—"}</td>
141
- <td className="px-5 py-3 whitespace-nowrap text-xs">
142
- {standbyName ? (
143
- <div>
144
- <span className="text-green-600 dark:text-green-400">HA enabled</span>
145
- <div className="text-gray-400 dark:text-gray-500 mt-0.5">standby: {standbyName}</div>
146
- {c.ha?.standbyRegion && <div className="text-gray-400 dark:text-gray-500">{c.ha.standbyRegion}</div>}
147
- </div>
148
- ) : primaryName ? (
149
- <div>
150
- <span className="text-yellow-600 dark:text-yellow-400">Standby of</span>
151
- <div className="text-gray-400 dark:text-gray-500 mt-0.5">{primaryName}</div>
152
- </div>
153
- ) : (
154
- <span className="text-gray-400 dark:text-gray-500">—</span>
155
- )}
156
- </td>
157
- <td className="px-5 py-3 whitespace-nowrap text-xs">
158
- {c.storageHA ? (
159
- <div>
160
- <div className="font-mono">{c.storageHA.sourceAccount}</div>
161
- {c.storageHA.destAccount && (
162
- <div className="text-gray-400 dark:text-gray-500 mt-0.5">replica: {c.storageHA.destAccount} ({c.storageHA.destRegion})</div>
163
- )}
164
- </div>
165
- ) : c.storageAccount ? (
166
- <div className="font-mono">{c.storageAccount}</div>
167
- ) : (
168
- <span className="text-gray-400 dark:text-gray-500">—</span>
169
- )}
170
- </td>
171
- <td className="px-5 py-3 whitespace-nowrap text-xs">
172
- {c.vault ? (
173
- <div>
174
- <div className="font-mono">{c.vault.keyVaultName}</div>
175
- <div className="text-gray-400 dark:text-gray-500 mt-0.5">
176
- {c.vault.autoUnseal ? "auto-unseal" : "manual"} · {c.vault.initialized ? "initialized" : "pending"}
177
- </div>
178
- </div>
179
- ) : (
180
- <span className="text-gray-400 dark:text-gray-500">—</span>
181
- )}
182
- </td>
183
- <td className="px-5 py-3 whitespace-nowrap text-xs">
184
- {c.postgres ? (
185
- <div className="font-mono">{c.postgres.serverName}</div>
186
- ) : (
187
- <span className="text-gray-400 dark:text-gray-500">—</span>
188
- )}
189
- </td>
190
- <td className="px-5 py-3 whitespace-nowrap text-right">
191
- {canWrite && <button className="btn-xs bg-red-500/10 border-transparent text-red-500 rounded-lg shadow-xs hover:bg-red-500/20" onClick={() => setDeleteTarget({ type: "cluster", name: c.name })}>Delete</button>}
192
- </td>
193
- </tr>
194
- );
195
- })}
196
- </tbody>
197
- </table>
198
- </div>
199
- </div>
200
- </div>
201
- )}
202
-
203
- <LogPanel lines={actionLogs} title="Action Output" />
204
-
205
- {/* Delete modal */}
206
- {deleteTarget && (
207
- <div className="fixed inset-0 bg-gray-900/70 z-50 flex items-center justify-center p-4">
208
- <div className="bg-white dark:bg-gray-800 rounded-xl shadow-lg max-w-md w-full p-6">
209
- <h2 className="text-lg font-semibold text-gray-800 dark:text-gray-100 mb-2">Delete {deleteTarget.type === "vm" ? "VM" : "Cluster"}</h2>
210
- <p className="text-sm text-gray-500 dark:text-gray-400 mb-6">
211
- Are you sure you want to delete <strong className="text-gray-800 dark:text-gray-100">{deleteTarget.name}</strong>? This action cannot be undone.
212
- </p>
213
- <div className="flex justify-end gap-2">
214
- <button className="btn border-gray-200 dark:border-gray-700/60 text-gray-600 dark:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600" onClick={() => setDeleteTarget(null)}>Cancel</button>
215
- <button className="btn bg-red-500 text-white hover:bg-red-600" disabled={deleteRes.isPending} onClick={() => { setActionLogs([]); deleteRes.mutate({ type: deleteTarget.type, name: deleteTarget.name, onLine: onActionLine }, { onSuccess: () => setDeleteTarget(null) }); }}>
216
- {deleteRes.isPending ? "Deleting..." : "Delete"}
217
- </button>
218
- </div>
219
- </div>
220
- </div>
221
- )}
222
- </div>
223
- </main>
224
- </div>
225
- </div>
226
- );
227
- }
228
-
229
- export default Resources;
@@ -1,132 +0,0 @@
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;
@@ -1,174 +0,0 @@
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;
@@ -1,21 +0,0 @@
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;