@meshxdata/fops 0.1.55 → 0.1.58
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/CHANGELOG.md +191 -4
- package/package.json +1 -2
- package/src/commands/index.js +2 -0
- package/src/commands/k3s-cmd.js +124 -0
- package/src/commands/lifecycle.js +7 -0
- package/src/plugins/builtins/docker-compose.js +17 -35
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-openai.js +0 -3
- package/src/plugins/bundled/fops-plugin-azure/lib/commands/vm-cmds.js +5 -2
- package/src/plugins/bundled/fops-plugin-cloud/api.js +14 -0
- package/src/project.js +12 -7
- package/src/plugins/bundled/fops-plugin-cloud/ui/postcss.config.cjs +0 -5
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/App.jsx +0 -32
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/api/client.js +0 -114
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/api/queries.js +0 -111
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/components/LogPanel.jsx +0 -162
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/components/ThemeToggle.jsx +0 -46
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/css/additional-styles/utility-patterns.css +0 -147
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/css/style.css +0 -138
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/favicon.svg +0 -15
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/lib/utils.ts +0 -19
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/main.jsx +0 -25
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/pages/Audit.jsx +0 -164
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/pages/Costs.jsx +0 -305
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/pages/CreateResource.jsx +0 -285
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/pages/Fleet.jsx +0 -307
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/pages/Resources.jsx +0 -229
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/partials/Header.jsx +0 -132
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/partials/Sidebar.jsx +0 -174
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/partials/SidebarLinkGroup.jsx +0 -21
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/utils/AuthContext.jsx +0 -170
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/utils/Info.jsx +0 -49
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/utils/ThemeContext.jsx +0 -37
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/utils/Transition.jsx +0 -116
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/utils/Utils.js +0 -63
|
@@ -1,307 +0,0 @@
|
|
|
1
|
-
import React, { useState, useCallback } from "react";
|
|
2
|
-
import Sidebar from "../partials/Sidebar";
|
|
3
|
-
import Header from "../partials/Header";
|
|
4
|
-
import LogPanel from "../components/LogPanel";
|
|
5
|
-
import { useFleet, useSyncResources, useFeatureFlags, useSetFeatureFlags, useDeploy, useGrantAdmin } from "../api/queries";
|
|
6
|
-
import { useAuthContext } from "../utils/AuthContext";
|
|
7
|
-
|
|
8
|
-
const SVC_LABELS = { be: "Backend", fe: "Frontend", pr: "Processor", wa: "Watcher", sc: "Scheduler", se: "Storage" };
|
|
9
|
-
|
|
10
|
-
function Fleet() {
|
|
11
|
-
const [sidebarOpen, setSidebarOpen] = useState(false);
|
|
12
|
-
const [syncLogs, setSyncLogs] = useState([]);
|
|
13
|
-
const [actionLogs, setActionLogs] = useState([]);
|
|
14
|
-
const [flagsVm, setFlagsVm] = useState(null);
|
|
15
|
-
const [grantVm, setGrantVm] = useState(null);
|
|
16
|
-
const [grantEmail, setGrantEmail] = useState("");
|
|
17
|
-
const { data, isLoading } = useFleet();
|
|
18
|
-
const sync = useSyncResources();
|
|
19
|
-
const deploy = useDeploy();
|
|
20
|
-
const grantAdmin = useGrantAdmin();
|
|
21
|
-
const setFlags = useSetFeatureFlags();
|
|
22
|
-
const { data: flagsData, isLoading: flagsLoading } = useFeatureFlags(flagsVm);
|
|
23
|
-
const [pendingFlags, setPendingFlags] = useState({});
|
|
24
|
-
const { canWrite, canDeploy } = useAuthContext();
|
|
25
|
-
const onSyncLine = useCallback((text, type) => setSyncLogs((p) => [...p, { text, type }]), []);
|
|
26
|
-
const onActionLine = useCallback((text, type) => setActionLogs((p) => [...p, { text, type }]), []);
|
|
27
|
-
|
|
28
|
-
const fleetVms = [];
|
|
29
|
-
const fleetClusters = [];
|
|
30
|
-
if (data) {
|
|
31
|
-
for (const [provider, fleet] of Object.entries(data)) {
|
|
32
|
-
for (const [name, vm] of Object.entries(fleet?.vms || {})) {
|
|
33
|
-
fleetVms.push({ name, provider, ...vm });
|
|
34
|
-
}
|
|
35
|
-
for (const [name, cluster] of Object.entries(fleet?.clusters || {})) {
|
|
36
|
-
fleetClusters.push({ name, provider, ...cluster });
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return (
|
|
42
|
-
<div className="flex h-screen overflow-hidden">
|
|
43
|
-
<Sidebar sidebarOpen={sidebarOpen} setSidebarOpen={setSidebarOpen} />
|
|
44
|
-
<div className="relative flex flex-col flex-1 overflow-y-auto overflow-x-hidden">
|
|
45
|
-
<Header sidebarOpen={sidebarOpen} setSidebarOpen={setSidebarOpen} />
|
|
46
|
-
<main className="grow">
|
|
47
|
-
<div className="px-4 sm:px-6 lg:px-8 py-8 w-full max-w-9xl mx-auto">
|
|
48
|
-
<div className="sm:flex sm:justify-between sm:items-center mb-8">
|
|
49
|
-
<div className="mb-4 sm:mb-0">
|
|
50
|
-
<h1 className="text-2xl md:text-3xl text-gray-800 dark:text-gray-100 font-bold">Fleet</h1>
|
|
51
|
-
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">Foundation stack status across Sandbox and Distributed foundations</p>
|
|
52
|
-
</div>
|
|
53
|
-
{canWrite && (
|
|
54
|
-
<button className="btn bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700/60 hover:border-gray-300 dark:hover:border-gray-600 text-gray-400 dark:text-gray-500" onClick={() => { setSyncLogs([]); sync.mutate({ onLine: onSyncLine }); }} disabled={sync.isPending}>
|
|
55
|
-
<svg className={`fill-current shrink-0 ${sync.isPending ? "animate-spin" : ""}`} width="16" height="16" viewBox="0 0 16 16"><path d="M14.15 4.92A7.05 7.05 0 0 0 8.5 1a7 7 0 1 0 6.58 9.39 1 1 0 1 0-1.88-.68A5 5 0 1 1 8.5 3a5.07 5.07 0 0 1 4.04 2.04L11 5.5V9h3.5L13 7.5l1.15-2.58Z" /></svg>
|
|
56
|
-
<span>Sync Fleet</span>
|
|
57
|
-
</button>
|
|
58
|
-
)}
|
|
59
|
-
</div>
|
|
60
|
-
|
|
61
|
-
{isLoading ? (
|
|
62
|
-
<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 fleet data...</div>
|
|
63
|
-
) : (fleetVms.length === 0 && fleetClusters.length === 0) ? (
|
|
64
|
-
<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">No fleet data available. Run a sync to discover resources.</div>
|
|
65
|
-
) : (
|
|
66
|
-
<div className="grid grid-cols-12 gap-6">
|
|
67
|
-
{/* VMs table */}
|
|
68
|
-
{fleetVms.length > 0 && (
|
|
69
|
-
<div className="col-span-full bg-white dark:bg-gray-800 shadow-xs rounded-xl">
|
|
70
|
-
<header className="px-5 py-4 border-b border-gray-100 dark:border-gray-700/60">
|
|
71
|
-
<h2 className="font-semibold text-gray-800 dark:text-gray-100">Sandbox Foundations</h2>
|
|
72
|
-
</header>
|
|
73
|
-
<div className="overflow-x-auto">
|
|
74
|
-
<table className="table-auto w-full dark:text-gray-300">
|
|
75
|
-
<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">
|
|
76
|
-
<tr>
|
|
77
|
-
<th className="px-5 py-3 font-semibold text-left whitespace-nowrap">Name</th>
|
|
78
|
-
<th className="px-5 py-3 font-semibold text-left whitespace-nowrap">Location</th>
|
|
79
|
-
<th className="px-5 py-3 font-semibold text-left whitespace-nowrap">Branch</th>
|
|
80
|
-
<th className="px-5 py-3 font-semibold text-center whitespace-nowrap">Running</th>
|
|
81
|
-
{Object.keys(SVC_LABELS).map((k) => (
|
|
82
|
-
<th key={k} className="px-3 py-3 font-semibold text-center whitespace-nowrap">{SVC_LABELS[k]}</th>
|
|
83
|
-
))}
|
|
84
|
-
<th className="px-5 py-3 font-semibold text-left whitespace-nowrap">Status</th>
|
|
85
|
-
<th className="px-5 py-3 font-semibold text-right whitespace-nowrap">Actions</th>
|
|
86
|
-
</tr>
|
|
87
|
-
</thead>
|
|
88
|
-
<tbody className="text-sm divide-y divide-gray-100 dark:divide-gray-700/60">
|
|
89
|
-
{fleetVms.map((vm) => (
|
|
90
|
-
<tr key={`${vm.provider}-${vm.name}`}>
|
|
91
|
-
<td className="px-5 py-3 whitespace-nowrap">
|
|
92
|
-
<div className="font-medium text-gray-800 dark:text-gray-100">{vm.name}</div>
|
|
93
|
-
{vm.publicUrl && <div className="text-xs text-gray-400 dark:text-gray-500 font-mono">{vm.publicUrl}</div>}
|
|
94
|
-
</td>
|
|
95
|
-
<td className="px-5 py-3 whitespace-nowrap">{vm.location || "—"}</td>
|
|
96
|
-
<td className="px-5 py-3 whitespace-nowrap font-mono text-xs">
|
|
97
|
-
{vm.branch || "—"}
|
|
98
|
-
{vm.sha && <span className="text-gray-400 dark:text-gray-500 ml-1">({vm.sha})</span>}
|
|
99
|
-
</td>
|
|
100
|
-
<td className="px-5 py-3 whitespace-nowrap text-center">
|
|
101
|
-
{vm.running != null ? (
|
|
102
|
-
<>
|
|
103
|
-
<span className="text-green-500 font-medium">{vm.running}</span>
|
|
104
|
-
<span className="text-gray-400"> / {vm.total || ((vm.running || 0) + (vm.exited || 0))}</span>
|
|
105
|
-
</>
|
|
106
|
-
) : "—"}
|
|
107
|
-
</td>
|
|
108
|
-
{Object.keys(SVC_LABELS).map((k) => {
|
|
109
|
-
const svc = vm.services?.[k];
|
|
110
|
-
// Support both old string format and new {tag, sha} format
|
|
111
|
-
const tag = typeof svc === "object" ? svc?.tag : null;
|
|
112
|
-
const sha = typeof svc === "object" ? svc?.sha : (typeof svc === "string" ? svc : null);
|
|
113
|
-
return (
|
|
114
|
-
<td key={k} className="px-3 py-3 whitespace-nowrap text-center font-mono text-xs">
|
|
115
|
-
{(tag || sha) ? (
|
|
116
|
-
<div>
|
|
117
|
-
{tag && <div className="text-gray-700 dark:text-gray-300">{tag}</div>}
|
|
118
|
-
{sha && <div className="text-gray-400 dark:text-gray-500 text-[10px]">{sha}</div>}
|
|
119
|
-
</div>
|
|
120
|
-
) : (
|
|
121
|
-
<span className="text-gray-300 dark:text-gray-600">—</span>
|
|
122
|
-
)}
|
|
123
|
-
</td>
|
|
124
|
-
);
|
|
125
|
-
})}
|
|
126
|
-
<td className="px-5 py-3 whitespace-nowrap">
|
|
127
|
-
<span className={`text-xs font-medium px-2 py-0.5 rounded-full ${vm.status === "up" ? "bg-green-500/20 text-green-700 dark:text-green-400" : "bg-gray-200 dark:bg-gray-700 text-gray-500"}`}>
|
|
128
|
-
{vm.status || "unknown"}
|
|
129
|
-
</span>
|
|
130
|
-
</td>
|
|
131
|
-
<td className="px-5 py-3 whitespace-nowrap text-right">
|
|
132
|
-
<div className="flex items-center justify-end gap-1">
|
|
133
|
-
{canDeploy && <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([]); deploy.mutate({ vmName: vm.name, onLine: onActionLine }); }} disabled={deploy.isPending}>Deploy</button>}
|
|
134
|
-
{canWrite && <button className="btn-xs bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700/60 text-amber-600 dark:text-amber-400 rounded-lg shadow-xs hover:border-gray-300 dark:hover:border-gray-600" onClick={() => { setGrantVm(vm.name); setGrantEmail(""); }}>Grant Admin</button>}
|
|
135
|
-
{canWrite && <button 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" onClick={() => { setFlagsVm(vm.name); setPendingFlags({}); }}>Flags</button>}
|
|
136
|
-
</div>
|
|
137
|
-
</td>
|
|
138
|
-
</tr>
|
|
139
|
-
))}
|
|
140
|
-
</tbody>
|
|
141
|
-
</table>
|
|
142
|
-
</div>
|
|
143
|
-
</div>
|
|
144
|
-
)}
|
|
145
|
-
|
|
146
|
-
{/* AKS Clusters table */}
|
|
147
|
-
{fleetClusters.length > 0 && (
|
|
148
|
-
<div className="col-span-full bg-white dark:bg-gray-800 shadow-xs rounded-xl">
|
|
149
|
-
<header className="px-5 py-4 border-b border-gray-100 dark:border-gray-700/60">
|
|
150
|
-
<h2 className="font-semibold text-gray-800 dark:text-gray-100">Distributed Foundations</h2>
|
|
151
|
-
</header>
|
|
152
|
-
<div className="overflow-x-auto">
|
|
153
|
-
<table className="table-auto w-full dark:text-gray-300">
|
|
154
|
-
<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">
|
|
155
|
-
<tr>
|
|
156
|
-
<th className="px-5 py-3 font-semibold text-left whitespace-nowrap">Cluster</th>
|
|
157
|
-
<th className="px-5 py-3 font-semibold text-left whitespace-nowrap">Location</th>
|
|
158
|
-
<th className="px-5 py-3 font-semibold text-left whitespace-nowrap">K8s</th>
|
|
159
|
-
<th className="px-5 py-3 font-semibold text-center whitespace-nowrap">Nodes</th>
|
|
160
|
-
{Object.keys(SVC_LABELS).map((k) => (
|
|
161
|
-
<th key={k} className="px-3 py-3 font-semibold text-center whitespace-nowrap">{SVC_LABELS[k]}</th>
|
|
162
|
-
))}
|
|
163
|
-
<th className="px-5 py-3 font-semibold text-left whitespace-nowrap">Flux</th>
|
|
164
|
-
<th className="px-5 py-3 font-semibold text-left whitespace-nowrap">Status</th>
|
|
165
|
-
</tr>
|
|
166
|
-
</thead>
|
|
167
|
-
<tbody className="text-sm divide-y divide-gray-100 dark:divide-gray-700/60">
|
|
168
|
-
{fleetClusters.map((c) => (
|
|
169
|
-
<tr key={`${c.provider}-${c.name}`}>
|
|
170
|
-
<td className="px-5 py-3 whitespace-nowrap">
|
|
171
|
-
<div className="flex items-center gap-2">
|
|
172
|
-
<span className="font-medium text-gray-800 dark:text-gray-100">{c.name}</span>
|
|
173
|
-
{c.active && <span className="text-xs font-medium px-1.5 py-0.5 rounded-full bg-violet-500/20 text-violet-600 dark:text-violet-400">active</span>}
|
|
174
|
-
</div>
|
|
175
|
-
</td>
|
|
176
|
-
<td className="px-5 py-3 whitespace-nowrap">{c.location || "—"}</td>
|
|
177
|
-
<td className="px-5 py-3 whitespace-nowrap font-mono text-xs">{c.kubernetesVersion || "—"}</td>
|
|
178
|
-
<td className="px-5 py-3 whitespace-nowrap text-center">{c.nodes ?? "—"}</td>
|
|
179
|
-
{Object.keys(SVC_LABELS).map((k) => {
|
|
180
|
-
const svc = c.services?.[k];
|
|
181
|
-
const tag = typeof svc === "object" ? svc?.tag : (typeof svc === "string" ? svc : null);
|
|
182
|
-
return (
|
|
183
|
-
<td key={k} className="px-3 py-3 whitespace-nowrap text-center font-mono text-xs">
|
|
184
|
-
{tag ? (
|
|
185
|
-
<span className="inline-flex px-1.5 py-0.5 rounded bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300">{tag}</span>
|
|
186
|
-
) : (
|
|
187
|
-
<span className="text-gray-300 dark:text-gray-600">—</span>
|
|
188
|
-
)}
|
|
189
|
-
</td>
|
|
190
|
-
);
|
|
191
|
-
})}
|
|
192
|
-
<td className="px-5 py-3 whitespace-nowrap text-xs">
|
|
193
|
-
{c.flux ? (
|
|
194
|
-
<span className="text-violet-500">{c.flux.owner}/{c.flux.repo}</span>
|
|
195
|
-
) : (
|
|
196
|
-
<span className="text-gray-300 dark:text-gray-600">—</span>
|
|
197
|
-
)}
|
|
198
|
-
</td>
|
|
199
|
-
<td className="px-5 py-3 whitespace-nowrap">
|
|
200
|
-
<span className={`text-xs font-medium px-2 py-0.5 rounded-full ${c.status === "Running" ? "bg-green-500/20 text-green-700 dark:text-green-400" : "bg-gray-200 dark:bg-gray-700 text-gray-500"}`}>
|
|
201
|
-
{c.status || "unknown"}
|
|
202
|
-
</span>
|
|
203
|
-
</td>
|
|
204
|
-
</tr>
|
|
205
|
-
))}
|
|
206
|
-
</tbody>
|
|
207
|
-
</table>
|
|
208
|
-
</div>
|
|
209
|
-
</div>
|
|
210
|
-
)}
|
|
211
|
-
</div>
|
|
212
|
-
)}
|
|
213
|
-
|
|
214
|
-
<LogPanel lines={syncLogs} title="Sync Output" />
|
|
215
|
-
<LogPanel lines={actionLogs} title="Action Output" />
|
|
216
|
-
|
|
217
|
-
{/* Feature Flags modal */}
|
|
218
|
-
{flagsVm && (
|
|
219
|
-
<div className="fixed inset-0 bg-gray-900/70 z-50 flex items-center justify-center p-4">
|
|
220
|
-
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-lg max-w-lg w-full p-6">
|
|
221
|
-
<h2 className="text-lg font-semibold text-gray-800 dark:text-gray-100 mb-1">Feature Flags</h2>
|
|
222
|
-
<p className="text-sm text-gray-500 dark:text-gray-400 mb-5">{flagsVm}</p>
|
|
223
|
-
{flagsLoading ? (
|
|
224
|
-
<p className="text-sm text-gray-400 py-4 text-center">Loading flags...</p>
|
|
225
|
-
) : (
|
|
226
|
-
<div className="space-y-2 max-h-80 overflow-y-auto">
|
|
227
|
-
{(flagsData?.flags || []).map((flag) => {
|
|
228
|
-
const current = pendingFlags[flag.name] ?? flag.value;
|
|
229
|
-
return (
|
|
230
|
-
<label key={flag.name} className="flex items-center justify-between px-3 py-2 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/30 cursor-pointer">
|
|
231
|
-
<div>
|
|
232
|
-
<div className="text-sm font-medium text-gray-800 dark:text-gray-100">{flag.label}</div>
|
|
233
|
-
{flag.services.length > 0 && <div className="text-xs text-gray-400 dark:text-gray-500">{flag.services.join(", ")}</div>}
|
|
234
|
-
</div>
|
|
235
|
-
<input
|
|
236
|
-
type="checkbox"
|
|
237
|
-
className="form-checkbox"
|
|
238
|
-
checked={current}
|
|
239
|
-
onChange={(e) => setPendingFlags((p) => ({ ...p, [flag.name]: e.target.checked }))}
|
|
240
|
-
/>
|
|
241
|
-
</label>
|
|
242
|
-
);
|
|
243
|
-
})}
|
|
244
|
-
</div>
|
|
245
|
-
)}
|
|
246
|
-
<div className="flex justify-end gap-2 mt-5 pt-4 border-t border-gray-200 dark:border-gray-700/60">
|
|
247
|
-
<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={() => setFlagsVm(null)}>Cancel</button>
|
|
248
|
-
<button
|
|
249
|
-
className="btn bg-violet-500 text-white hover:bg-violet-600"
|
|
250
|
-
disabled={setFlags.isPending || Object.keys(pendingFlags).length === 0}
|
|
251
|
-
onClick={() => {
|
|
252
|
-
setActionLogs([]);
|
|
253
|
-
setFlags.mutate({ vmName: flagsVm, flags: pendingFlags, onLine: onActionLine }, {
|
|
254
|
-
onSuccess: () => { setFlagsVm(null); setPendingFlags({}); },
|
|
255
|
-
});
|
|
256
|
-
}}
|
|
257
|
-
>
|
|
258
|
-
{setFlags.isPending ? "Applying..." : "Apply"}
|
|
259
|
-
</button>
|
|
260
|
-
</div>
|
|
261
|
-
</div>
|
|
262
|
-
</div>
|
|
263
|
-
)}
|
|
264
|
-
{/* Grant Admin modal */}
|
|
265
|
-
{grantVm && (
|
|
266
|
-
<div className="fixed inset-0 bg-gray-900/70 z-50 flex items-center justify-center p-4">
|
|
267
|
-
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-lg max-w-md w-full p-6">
|
|
268
|
-
<h2 className="text-lg font-semibold text-gray-800 dark:text-gray-100 mb-1">Grant Admin</h2>
|
|
269
|
-
<p className="text-sm text-gray-500 dark:text-gray-400 mb-5">{grantVm}</p>
|
|
270
|
-
<div className="mb-4">
|
|
271
|
-
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Email (optional)</label>
|
|
272
|
-
<input
|
|
273
|
-
type="email"
|
|
274
|
-
className="form-input w-full"
|
|
275
|
-
placeholder="Leave empty to grant all users"
|
|
276
|
-
value={grantEmail}
|
|
277
|
-
onChange={(e) => setGrantEmail(e.target.value)}
|
|
278
|
-
/>
|
|
279
|
-
<p className="text-xs text-gray-400 dark:text-gray-500 mt-1">Leave empty to grant Foundation Admin to all non-system users</p>
|
|
280
|
-
</div>
|
|
281
|
-
<div className="flex justify-end gap-2 pt-4 border-t border-gray-200 dark:border-gray-700/60">
|
|
282
|
-
<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={() => setGrantVm(null)}>Cancel</button>
|
|
283
|
-
<button
|
|
284
|
-
className="btn bg-amber-500 text-white hover:bg-amber-600"
|
|
285
|
-
disabled={grantAdmin.isPending}
|
|
286
|
-
onClick={() => {
|
|
287
|
-
setActionLogs([]);
|
|
288
|
-
grantAdmin.mutate(
|
|
289
|
-
{ vmName: grantVm, username: grantEmail || undefined, onLine: onActionLine },
|
|
290
|
-
{ onSuccess: () => setGrantVm(null) },
|
|
291
|
-
);
|
|
292
|
-
}}
|
|
293
|
-
>
|
|
294
|
-
{grantAdmin.isPending ? "Granting..." : "Grant"}
|
|
295
|
-
</button>
|
|
296
|
-
</div>
|
|
297
|
-
</div>
|
|
298
|
-
</div>
|
|
299
|
-
)}
|
|
300
|
-
</div>
|
|
301
|
-
</main>
|
|
302
|
-
</div>
|
|
303
|
-
</div>
|
|
304
|
-
);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
export default Fleet;
|
|
@@ -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;
|