@meshxdata/fops 0.1.55 β 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.
- package/CHANGELOG.md +4 -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,285 +0,0 @@
|
|
|
1
|
-
import React, { useState, useCallback, useEffect, useRef } 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 { useCreateResource } from "../api/queries";
|
|
7
|
-
import { pollJob } from "../api/client";
|
|
8
|
-
|
|
9
|
-
const STORAGE_KEY = "cloud-provision-job";
|
|
10
|
-
|
|
11
|
-
const LOCATIONS = [
|
|
12
|
-
{ value: "uaenorth", label: "UAE North", flag: "π¦πͺ" },
|
|
13
|
-
{ value: "eastus", label: "East US", flag: "πΊπΈ" },
|
|
14
|
-
{ value: "westeurope", label: "West Europe", flag: "πͺπΊ" },
|
|
15
|
-
{ value: "northeurope", label: "North Europe", flag: "πͺπΊ" },
|
|
16
|
-
{ value: "southeastasia", label: "Southeast Asia", flag: "πΈπ¬" },
|
|
17
|
-
];
|
|
18
|
-
const VM_SIZES = [
|
|
19
|
-
{ value: "Standard_B2s", label: "B2s", spec: "2 vCPU Β· 4 GiB" },
|
|
20
|
-
{ value: "Standard_B2ms", label: "B2ms", spec: "2 vCPU Β· 8 GiB" },
|
|
21
|
-
{ value: "Standard_D2s_v5", label: "D2s v5", spec: "2 vCPU Β· 8 GiB" },
|
|
22
|
-
{ value: "Standard_D4s_v5", label: "D4s v5", spec: "4 vCPU Β· 16 GiB" },
|
|
23
|
-
];
|
|
24
|
-
|
|
25
|
-
const ALL_STEPS = ["Type", "Configure", "Review", "Deploy"];
|
|
26
|
-
|
|
27
|
-
// Shared tile class for selectable options
|
|
28
|
-
const tileBase = "text-left rounded-lg border transition-all cursor-pointer";
|
|
29
|
-
const tileActive = "border-violet-500 bg-violet-500/10 shadow-sm shadow-violet-500/10";
|
|
30
|
-
const tileInactive = "border-gray-200 dark:border-gray-700/60 hover:border-gray-300 dark:hover:border-gray-600 bg-gray-50 dark:bg-gray-900/50";
|
|
31
|
-
const tile = (active) => `${tileBase} ${active ? tileActive : tileInactive}`;
|
|
32
|
-
|
|
33
|
-
function CreateResource() {
|
|
34
|
-
const [sidebarOpen, setSidebarOpen] = useState(false);
|
|
35
|
-
const navigate = useNavigate();
|
|
36
|
-
const createResource = useCreateResource();
|
|
37
|
-
const [step, setStep] = useState(0);
|
|
38
|
-
const [form, setForm] = useState({ type: "vm", vmName: "", location: "uaenorth", vmSize: "Standard_B2s", resourceGroup: "", clusterName: "", nodeCount: 2, kubernetesVersion: "1.30" });
|
|
39
|
-
const [logs, setLogs] = useState([]);
|
|
40
|
-
const [reconnecting, setReconnecting] = useState(false);
|
|
41
|
-
const [reconnectDone, setReconnectDone] = useState(false);
|
|
42
|
-
const pollingRef = useRef(false);
|
|
43
|
-
const set = (f) => (e) => setForm((p) => ({ ...p, [f]: e.target.value }));
|
|
44
|
-
const canProceed = step === 0 ? true : step === 1 ? (form.type === "vm" ? form.vmName.length >= 3 : form.clusterName.length >= 3) : true;
|
|
45
|
-
|
|
46
|
-
const onLine = useCallback((text, type) => setLogs((prev) => [...prev, { text, type }]), []);
|
|
47
|
-
const onJobId = useCallback((jobId) => localStorage.setItem(STORAGE_KEY, jobId), []);
|
|
48
|
-
|
|
49
|
-
useEffect(() => {
|
|
50
|
-
const savedJobId = localStorage.getItem(STORAGE_KEY);
|
|
51
|
-
if (!savedJobId || pollingRef.current) return;
|
|
52
|
-
pollingRef.current = true;
|
|
53
|
-
setStep(3);
|
|
54
|
-
setReconnecting(true);
|
|
55
|
-
pollJob(savedJobId, (text, type) => setLogs((prev) => [...prev, { text, type }]))
|
|
56
|
-
.then(() => { setReconnecting(false); setReconnectDone(true); localStorage.removeItem(STORAGE_KEY); })
|
|
57
|
-
.catch(() => { localStorage.removeItem(STORAGE_KEY); setReconnecting(false); setStep(0); pollingRef.current = false; });
|
|
58
|
-
}, []);
|
|
59
|
-
|
|
60
|
-
const handleCreate = () => {
|
|
61
|
-
const body = form.type === "vm"
|
|
62
|
-
? { type: "vm", vmName: form.vmName, location: form.location, vmSize: form.vmSize, resourceGroup: form.resourceGroup || undefined }
|
|
63
|
-
: { type: "cluster", clusterName: form.clusterName, location: form.location, nodeCount: Number(form.nodeCount), kubernetesVersion: form.kubernetesVersion };
|
|
64
|
-
setLogs([]);
|
|
65
|
-
setStep(3);
|
|
66
|
-
createResource.mutate({ body, onLine, onJobId }, { onSuccess: () => localStorage.removeItem(STORAGE_KEY) });
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
const isRunning = reconnecting || createResource.isPending;
|
|
70
|
-
const isDone = reconnectDone || (!createResource.isPending && !createResource.isError && logs.length > 0);
|
|
71
|
-
|
|
72
|
-
return (
|
|
73
|
-
<div className="flex h-screen overflow-hidden">
|
|
74
|
-
<Sidebar sidebarOpen={sidebarOpen} setSidebarOpen={setSidebarOpen} />
|
|
75
|
-
<div className="relative flex flex-col flex-1 overflow-y-auto overflow-x-hidden">
|
|
76
|
-
<Header sidebarOpen={sidebarOpen} setSidebarOpen={setSidebarOpen} />
|
|
77
|
-
<main className="grow">
|
|
78
|
-
<div className="px-4 sm:px-6 lg:px-8 py-8 w-full max-w-4xl mx-auto">
|
|
79
|
-
|
|
80
|
-
{/* Pipeline header */}
|
|
81
|
-
<div className="flex items-center justify-between mb-6">
|
|
82
|
-
<div>
|
|
83
|
-
<h1 className="text-xl font-bold text-gray-800 dark:text-gray-100">Foundation Factory</h1>
|
|
84
|
-
<p className="text-sm text-gray-500 dark:text-gray-400 mt-0.5">Provision a Foundation environment</p>
|
|
85
|
-
</div>
|
|
86
|
-
{step < 3 && (
|
|
87
|
-
<button className="btn-sm border border-gray-200 dark:border-gray-700/60 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 rounded-lg" onClick={() => navigate("/")}>Cancel</button>
|
|
88
|
-
)}
|
|
89
|
-
</div>
|
|
90
|
-
|
|
91
|
-
{/* Pipeline stepper */}
|
|
92
|
-
<div className="flex items-center mb-8">
|
|
93
|
-
{ALL_STEPS.map((s, i) => {
|
|
94
|
-
const done = i < step || (i === 3 && isDone);
|
|
95
|
-
const active = i === step && !(i === 3 && isDone);
|
|
96
|
-
return (
|
|
97
|
-
<React.Fragment key={s}>
|
|
98
|
-
<div className="flex items-center gap-2">
|
|
99
|
-
<div className={`w-7 h-7 rounded-full flex items-center justify-center text-xs font-bold transition-all duration-300 ${
|
|
100
|
-
done ? "bg-green-500 text-white shadow-sm shadow-green-500/30"
|
|
101
|
-
: active ? "bg-violet-500 text-white shadow-sm shadow-violet-500/30 ring-4 ring-violet-500/20"
|
|
102
|
-
: "bg-gray-200 dark:bg-gray-800 text-gray-400 dark:text-gray-500 border border-gray-300 dark:border-gray-700/60"
|
|
103
|
-
}`}>
|
|
104
|
-
{done ? "β" : i + 1}
|
|
105
|
-
</div>
|
|
106
|
-
<span className={`text-sm font-medium hidden sm:inline ${
|
|
107
|
-
done ? "text-green-600 dark:text-green-500" : active ? "text-gray-800 dark:text-gray-100" : "text-gray-400 dark:text-gray-600"
|
|
108
|
-
}`}>{s}</span>
|
|
109
|
-
</div>
|
|
110
|
-
{i < ALL_STEPS.length - 1 && (
|
|
111
|
-
<div className={`flex-1 h-px mx-3 transition-colors duration-500 ${i < step ? "bg-green-500/40" : "bg-gray-200 dark:bg-gray-700/60"}`} />
|
|
112
|
-
)}
|
|
113
|
-
</React.Fragment>
|
|
114
|
-
);
|
|
115
|
-
})}
|
|
116
|
-
</div>
|
|
117
|
-
|
|
118
|
-
{/* Content card */}
|
|
119
|
-
<div className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700/60 shadow-xs rounded-xl overflow-hidden">
|
|
120
|
-
|
|
121
|
-
{/* Step 0: Type */}
|
|
122
|
-
{step === 0 && (
|
|
123
|
-
<div className="p-6">
|
|
124
|
-
<h2 className="text-sm font-semibold text-gray-400 dark:text-gray-500 uppercase tracking-wider mb-4">Select Target</h2>
|
|
125
|
-
<div className="grid grid-cols-2 gap-4">
|
|
126
|
-
{[
|
|
127
|
-
{ type: "vm", label: "Virtual Machine", desc: "Single-node Foundation on Azure VM", icon: "π₯" },
|
|
128
|
-
{ type: "cluster", label: "AKS Cluster", desc: "Distributed Foundation on managed Kubernetes", icon: "βΈ" },
|
|
129
|
-
].map((opt) => (
|
|
130
|
-
<button key={opt.type} onClick={() => setForm((p) => ({ ...p, type: opt.type }))} className={`${tile(form.type === opt.type)} p-5`}>
|
|
131
|
-
<div className="text-2xl mb-3">{opt.icon}</div>
|
|
132
|
-
<div className="text-sm font-semibold text-gray-800 dark:text-gray-100">{opt.label}</div>
|
|
133
|
-
<div className="text-xs text-gray-500 mt-1">{opt.desc}</div>
|
|
134
|
-
</button>
|
|
135
|
-
))}
|
|
136
|
-
</div>
|
|
137
|
-
</div>
|
|
138
|
-
)}
|
|
139
|
-
|
|
140
|
-
{/* Step 1: VM Config */}
|
|
141
|
-
{step === 1 && form.type === "vm" && (
|
|
142
|
-
<div className="p-6 space-y-5">
|
|
143
|
-
<h2 className="text-sm font-semibold text-gray-400 dark:text-gray-500 uppercase tracking-wider mb-1">VM Configuration</h2>
|
|
144
|
-
<Field label="Name" hint="Unique identifier for this VM">
|
|
145
|
-
<input className="field-input" placeholder="my-foundation-vm" value={form.vmName} onChange={set("vmName")} />
|
|
146
|
-
</Field>
|
|
147
|
-
<Field label="Region">
|
|
148
|
-
<div className="grid grid-cols-3 gap-2">
|
|
149
|
-
{LOCATIONS.map((l) => (
|
|
150
|
-
<button key={l.value} onClick={() => setForm((p) => ({ ...p, location: l.value }))} className={`${tile(form.location === l.value)} px-3 py-2.5 text-sm`}>
|
|
151
|
-
<span className="mr-1.5">{l.flag}</span>
|
|
152
|
-
<span className={form.location === l.value ? "text-gray-800 dark:text-gray-100" : "text-gray-600 dark:text-gray-400"}>{l.label}</span>
|
|
153
|
-
</button>
|
|
154
|
-
))}
|
|
155
|
-
</div>
|
|
156
|
-
</Field>
|
|
157
|
-
<Field label="Size">
|
|
158
|
-
<div className="grid grid-cols-2 gap-2">
|
|
159
|
-
{VM_SIZES.map((s) => (
|
|
160
|
-
<button key={s.value} onClick={() => setForm((p) => ({ ...p, vmSize: s.value }))} className={`${tile(form.vmSize === s.value)} px-3 py-2.5 text-sm`}>
|
|
161
|
-
<div className={`font-medium ${form.vmSize === s.value ? "text-gray-800 dark:text-gray-100" : "text-gray-700 dark:text-gray-200"}`}>{s.label}</div>
|
|
162
|
-
<div className="text-xs text-gray-400 dark:text-gray-500 mt-0.5">{s.spec}</div>
|
|
163
|
-
</button>
|
|
164
|
-
))}
|
|
165
|
-
</div>
|
|
166
|
-
</Field>
|
|
167
|
-
<Field label="Resource Group" hint="Leave empty for auto-generated">
|
|
168
|
-
<input className="field-input" placeholder="FOUNDATION-VM-RG" value={form.resourceGroup} onChange={set("resourceGroup")} />
|
|
169
|
-
</Field>
|
|
170
|
-
</div>
|
|
171
|
-
)}
|
|
172
|
-
|
|
173
|
-
{/* Step 1: Cluster Config */}
|
|
174
|
-
{step === 1 && form.type === "cluster" && (
|
|
175
|
-
<div className="p-6 space-y-5">
|
|
176
|
-
<h2 className="text-sm font-semibold text-gray-400 dark:text-gray-500 uppercase tracking-wider mb-1">Cluster Configuration</h2>
|
|
177
|
-
<Field label="Cluster Name">
|
|
178
|
-
<input className="field-input" placeholder="my-aks-cluster" value={form.clusterName} onChange={set("clusterName")} />
|
|
179
|
-
</Field>
|
|
180
|
-
<Field label="Region">
|
|
181
|
-
<div className="grid grid-cols-3 gap-2">
|
|
182
|
-
{LOCATIONS.map((l) => (
|
|
183
|
-
<button key={l.value} onClick={() => setForm((p) => ({ ...p, location: l.value }))} className={`${tile(form.location === l.value)} px-3 py-2.5 text-sm`}>
|
|
184
|
-
<span className="mr-1.5">{l.flag}</span>
|
|
185
|
-
<span className={form.location === l.value ? "text-gray-800 dark:text-gray-100" : "text-gray-600 dark:text-gray-400"}>{l.label}</span>
|
|
186
|
-
</button>
|
|
187
|
-
))}
|
|
188
|
-
</div>
|
|
189
|
-
</Field>
|
|
190
|
-
<Field label="Node Count">
|
|
191
|
-
<input className="field-input" type="number" min={1} max={20} value={form.nodeCount} onChange={set("nodeCount")} />
|
|
192
|
-
</Field>
|
|
193
|
-
<Field label="Kubernetes Version">
|
|
194
|
-
<input className="field-input" placeholder="1.30" value={form.kubernetesVersion} onChange={set("kubernetesVersion")} />
|
|
195
|
-
</Field>
|
|
196
|
-
</div>
|
|
197
|
-
)}
|
|
198
|
-
|
|
199
|
-
{/* Step 2: Review */}
|
|
200
|
-
{step === 2 && (
|
|
201
|
-
<div className="p-6">
|
|
202
|
-
<h2 className="text-sm font-semibold text-gray-400 dark:text-gray-500 uppercase tracking-wider mb-4">Review Deployment</h2>
|
|
203
|
-
<div className="bg-gray-50 dark:bg-gray-900/60 rounded-lg border border-gray-200 dark:border-gray-700/60 divide-y divide-gray-200 dark:divide-gray-700/60">
|
|
204
|
-
{(form.type === "vm"
|
|
205
|
-
? [
|
|
206
|
-
["Target", "Virtual Machine"],
|
|
207
|
-
["Name", form.vmName],
|
|
208
|
-
["Region", LOCATIONS.find(l => l.value === form.location)?.label || form.location],
|
|
209
|
-
["Size", (VM_SIZES.find(s => s.value === form.vmSize)?.label || "") + " β " + (VM_SIZES.find(s => s.value === form.vmSize)?.spec || form.vmSize)],
|
|
210
|
-
...(form.resourceGroup ? [["Resource Group", form.resourceGroup]] : []),
|
|
211
|
-
]
|
|
212
|
-
: [
|
|
213
|
-
["Target", "AKS Cluster"],
|
|
214
|
-
["Name", form.clusterName],
|
|
215
|
-
["Region", LOCATIONS.find(l => l.value === form.location)?.label || form.location],
|
|
216
|
-
["Nodes", String(form.nodeCount)],
|
|
217
|
-
["K8s Version", form.kubernetesVersion],
|
|
218
|
-
]
|
|
219
|
-
).map(([label, value]) => (
|
|
220
|
-
<div key={label} className="flex justify-between items-center px-4 py-3">
|
|
221
|
-
<span className="text-xs font-medium text-gray-400 dark:text-gray-500 uppercase tracking-wider">{label}</span>
|
|
222
|
-
<span className="text-sm font-mono text-gray-800 dark:text-gray-200">{value}</span>
|
|
223
|
-
</div>
|
|
224
|
-
))}
|
|
225
|
-
</div>
|
|
226
|
-
{createResource.isError && <p className="text-sm text-red-500 mt-4">{createResource.error?.message || "Failed"}</p>}
|
|
227
|
-
</div>
|
|
228
|
-
)}
|
|
229
|
-
|
|
230
|
-
{/* Step 3: Deploy */}
|
|
231
|
-
{step === 3 && (
|
|
232
|
-
<div className="p-6">
|
|
233
|
-
<div className="flex items-center gap-3 mb-1">
|
|
234
|
-
{isRunning && <div className="w-4 h-4 border-2 border-violet-500 border-t-transparent rounded-full animate-spin" />}
|
|
235
|
-
<h2 className="text-sm font-semibold text-gray-400 dark:text-gray-500 uppercase tracking-wider">
|
|
236
|
-
{isRunning ? (reconnecting ? "Reconnecting..." : "Deploying") : isDone ? "Deployment Complete" : "Failed"}
|
|
237
|
-
</h2>
|
|
238
|
-
</div>
|
|
239
|
-
<LogPanel lines={logs} title="" isDone={isDone} showPhases />
|
|
240
|
-
{!isRunning && (
|
|
241
|
-
<div className="mt-5">
|
|
242
|
-
<button className="btn bg-violet-500 text-white hover:bg-violet-600" onClick={() => navigate("/")}>Go to Registry</button>
|
|
243
|
-
</div>
|
|
244
|
-
)}
|
|
245
|
-
</div>
|
|
246
|
-
)}
|
|
247
|
-
|
|
248
|
-
{/* Nav bar */}
|
|
249
|
-
{step < 3 && (
|
|
250
|
-
<div className="flex justify-between items-center px-6 py-4 border-t border-gray-100 dark:border-gray-700/60 bg-gray-50 dark:bg-gray-900/40">
|
|
251
|
-
<button
|
|
252
|
-
className="text-sm text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition disabled:opacity-30"
|
|
253
|
-
onClick={() => setStep((s) => s - 1)}
|
|
254
|
-
disabled={step === 0}
|
|
255
|
-
>← Back</button>
|
|
256
|
-
{step < 2 ? (
|
|
257
|
-
<button className="btn bg-violet-500 text-white hover:bg-violet-600 disabled:opacity-40" onClick={() => setStep((s) => s + 1)} disabled={!canProceed}>
|
|
258
|
-
Continue →
|
|
259
|
-
</button>
|
|
260
|
-
) : (
|
|
261
|
-
<button className="btn bg-green-600 text-white hover:bg-green-500" onClick={handleCreate}>
|
|
262
|
-
Deploy →
|
|
263
|
-
</button>
|
|
264
|
-
)}
|
|
265
|
-
</div>
|
|
266
|
-
)}
|
|
267
|
-
</div>
|
|
268
|
-
</div>
|
|
269
|
-
</main>
|
|
270
|
-
</div>
|
|
271
|
-
</div>
|
|
272
|
-
);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
function Field({ label, hint, children }) {
|
|
276
|
-
return (
|
|
277
|
-
<div>
|
|
278
|
-
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">{label}</label>
|
|
279
|
-
{children}
|
|
280
|
-
{hint && <p className="text-xs text-gray-400 dark:text-gray-600 mt-1.5">{hint}</p>}
|
|
281
|
-
</div>
|
|
282
|
-
);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
export default CreateResource;
|
|
@@ -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;
|