more-compute 0.4.3__py3-none-any.whl → 0.5.0__py3-none-any.whl
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.
- frontend/app/globals.css +734 -27
- frontend/app/layout.tsx +13 -3
- frontend/components/Notebook.tsx +2 -14
- frontend/components/cell/MonacoCell.tsx +99 -5
- frontend/components/layout/Sidebar.tsx +39 -4
- frontend/components/panels/ClaudePanel.tsx +461 -0
- frontend/components/popups/ComputePopup.tsx +739 -418
- frontend/components/popups/FilterPopup.tsx +305 -189
- frontend/components/popups/MetricsPopup.tsx +20 -1
- frontend/components/popups/ProviderConfigModal.tsx +322 -0
- frontend/components/popups/ProviderDropdown.tsx +398 -0
- frontend/components/popups/SettingsPopup.tsx +1 -1
- frontend/contexts/ClaudeContext.tsx +392 -0
- frontend/contexts/PodWebSocketContext.tsx +16 -21
- frontend/hooks/useInlineDiff.ts +269 -0
- frontend/lib/api.ts +323 -12
- frontend/lib/settings.ts +5 -0
- frontend/lib/websocket-native.ts +4 -8
- frontend/lib/websocket.ts +1 -2
- frontend/package-lock.json +733 -36
- frontend/package.json +2 -0
- frontend/public/assets/icons/providers/lambda_labs.svg +22 -0
- frontend/public/assets/icons/providers/prime_intellect.svg +18 -0
- frontend/public/assets/icons/providers/runpod.svg +9 -0
- frontend/public/assets/icons/providers/vastai.svg +1 -0
- frontend/settings.md +54 -0
- frontend/tsconfig.tsbuildinfo +1 -0
- frontend/types/claude.ts +194 -0
- kernel_run.py +13 -0
- {more_compute-0.4.3.dist-info → more_compute-0.5.0.dist-info}/METADATA +53 -11
- {more_compute-0.4.3.dist-info → more_compute-0.5.0.dist-info}/RECORD +56 -37
- {more_compute-0.4.3.dist-info → more_compute-0.5.0.dist-info}/WHEEL +1 -1
- morecompute/__init__.py +1 -1
- morecompute/__version__.py +1 -1
- morecompute/execution/executor.py +24 -67
- morecompute/execution/worker.py +6 -72
- morecompute/models/api_models.py +62 -0
- morecompute/notebook.py +11 -0
- morecompute/server.py +641 -133
- morecompute/services/claude_service.py +392 -0
- morecompute/services/pod_manager.py +168 -67
- morecompute/services/pod_monitor.py +67 -39
- morecompute/services/prime_intellect.py +0 -4
- morecompute/services/providers/__init__.py +92 -0
- morecompute/services/providers/base_provider.py +336 -0
- morecompute/services/providers/lambda_labs_provider.py +394 -0
- morecompute/services/providers/provider_factory.py +194 -0
- morecompute/services/providers/runpod_provider.py +504 -0
- morecompute/services/providers/vastai_provider.py +407 -0
- morecompute/utils/cell_magics.py +0 -3
- morecompute/utils/config_util.py +93 -3
- morecompute/utils/special_commands.py +5 -32
- morecompute/utils/version_check.py +117 -0
- frontend/styling_README.md +0 -23
- {more_compute-0.4.3.dist-info/licenses → more_compute-0.5.0.dist-info}/LICENSE +0 -0
- {more_compute-0.4.3.dist-info → more_compute-0.5.0.dist-info}/entry_points.txt +0 -0
- {more_compute-0.4.3.dist-info → more_compute-0.5.0.dist-info}/top_level.txt +0 -0
|
@@ -1,34 +1,31 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
Plus,
|
|
6
|
-
Activity,
|
|
7
|
-
Search,
|
|
8
|
-
Filter,
|
|
9
|
-
} from "lucide-react";
|
|
1
|
+
import ConfirmModal from "@/components/modals/ConfirmModal";
|
|
2
|
+
import ErrorModal from "@/components/modals/ErrorModal";
|
|
3
|
+
import SuccessModal from "@/components/modals/SuccessModal";
|
|
4
|
+
import { usePodWebSocket } from "@/contexts/PodWebSocketContext";
|
|
10
5
|
import {
|
|
11
|
-
|
|
12
|
-
fetchGpuConfig,
|
|
13
|
-
setGpuApiKey,
|
|
14
|
-
fetchGpuAvailability,
|
|
15
|
-
createGpuPod,
|
|
16
|
-
deleteGpuPod,
|
|
17
|
-
connectToPod,
|
|
6
|
+
CreatePodRequest,
|
|
18
7
|
disconnectFromPod,
|
|
8
|
+
fetchGpuConfig,
|
|
19
9
|
getPodConnectionStatus,
|
|
20
|
-
PodResponse,
|
|
21
|
-
PodsListParams,
|
|
22
10
|
GpuAvailability,
|
|
23
11
|
GpuAvailabilityParams,
|
|
24
|
-
CreatePodRequest,
|
|
25
12
|
PodConnectionStatus,
|
|
13
|
+
PodResponse,
|
|
14
|
+
PodsListParams,
|
|
15
|
+
setGpuApiKey,
|
|
16
|
+
// Multi-provider API
|
|
17
|
+
ProviderInfo,
|
|
18
|
+
fetchProviderGpuAvailability,
|
|
19
|
+
fetchProviderPods,
|
|
20
|
+
createProviderPod,
|
|
21
|
+
deleteProviderPod,
|
|
22
|
+
connectToProviderPod,
|
|
26
23
|
} from "@/lib/api";
|
|
27
|
-
import
|
|
28
|
-
import
|
|
29
|
-
import ConfirmModal from "@/components/modals/ConfirmModal";
|
|
24
|
+
import { ExternalLink, Filter } from "lucide-react";
|
|
25
|
+
import React, { useEffect, useMemo, useState } from "react";
|
|
30
26
|
import FilterPopup from "./FilterPopup";
|
|
31
|
-
import
|
|
27
|
+
import ProviderDropdown from "./ProviderDropdown";
|
|
28
|
+
import ProviderConfigModal from "./ProviderConfigModal";
|
|
32
29
|
|
|
33
30
|
interface GPUPod {
|
|
34
31
|
id: string;
|
|
@@ -71,13 +68,25 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
71
68
|
const [creatingPodId, setCreatingPodId] = useState<string | null>(null);
|
|
72
69
|
const [podCreationError, setPodCreationError] = useState<string | null>(null);
|
|
73
70
|
const [deletingPodId, setDeletingPodId] = useState<string | null>(null);
|
|
74
|
-
const [connectionHealth, setConnectionHealth] = useState<
|
|
71
|
+
const [connectionHealth, setConnectionHealth] = useState<
|
|
72
|
+
"healthy" | "unhealthy" | "unknown"
|
|
73
|
+
>("unknown");
|
|
75
74
|
const [searchQuery, setSearchQuery] = useState("");
|
|
76
75
|
const [discoveredPod, setDiscoveredPod] = useState<GPUPod | null>(null);
|
|
76
|
+
const [showApiKeyInput, setShowApiKeyInput] = useState(false);
|
|
77
77
|
|
|
78
78
|
// Filter popup state
|
|
79
79
|
const [showFilterPopup, setShowFilterPopup] = useState(false);
|
|
80
80
|
|
|
81
|
+
// Provider state
|
|
82
|
+
const [selectedProvider, setSelectedProvider] = useState<ProviderInfo | null>(null);
|
|
83
|
+
const [showProviderConfig, setShowProviderConfig] = useState(false);
|
|
84
|
+
const [providerToConfig, setProviderToConfig] = useState<ProviderInfo | null>(null);
|
|
85
|
+
|
|
86
|
+
// Loading error states
|
|
87
|
+
const [podsLoadError, setPodsLoadError] = useState<string | null>(null);
|
|
88
|
+
const [gpusLoadError, setGpusLoadError] = useState<string | null>(null);
|
|
89
|
+
|
|
81
90
|
// Error modal state
|
|
82
91
|
const [errorModal, setErrorModal] = useState<{
|
|
83
92
|
isOpen: boolean;
|
|
@@ -165,11 +174,10 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
165
174
|
setConnectionHealth("healthy");
|
|
166
175
|
} else if (!status.connected && status.pod) {
|
|
167
176
|
// Discovered running pod but not connected (backend restart scenario)
|
|
168
|
-
console.log("[COMPUTE POPUP] Discovered running pod after restart:", status.pod);
|
|
169
177
|
// Only show discovered pod warning if it's not already in the pods list
|
|
170
178
|
// (This can happen if pods haven't loaded yet)
|
|
171
179
|
const pod = status.pod;
|
|
172
|
-
const alreadyInList = gpuPods.some(p => p.id === pod.id);
|
|
180
|
+
const alreadyInList = gpuPods.some((p) => p.id === pod.id);
|
|
173
181
|
if (!alreadyInList) {
|
|
174
182
|
setDiscoveredPod({
|
|
175
183
|
id: pod.id,
|
|
@@ -191,20 +199,45 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
191
199
|
checkApiConfig();
|
|
192
200
|
}, []);
|
|
193
201
|
|
|
194
|
-
const loadGPUPods = async (params?: PodsListParams) => {
|
|
202
|
+
const loadGPUPods = async (params?: PodsListParams, provider?: ProviderInfo | null) => {
|
|
203
|
+
// Use passed provider or fall back to state
|
|
204
|
+
const activeProvider = provider !== undefined ? provider : selectedProvider;
|
|
205
|
+
|
|
206
|
+
// Require a configured provider - don't load pods without one
|
|
207
|
+
if (!activeProvider || !activeProvider.configured) {
|
|
208
|
+
setPods([]);
|
|
209
|
+
setPodsLoadError(null);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
195
213
|
setLoading(true);
|
|
214
|
+
setPodsLoadError(null);
|
|
196
215
|
try {
|
|
197
|
-
const response = await
|
|
216
|
+
const response = await fetchProviderPods(activeProvider.name, params || { limit: 100 });
|
|
217
|
+
|
|
198
218
|
const pods = (response.data || []).map((pod: PodResponse) => {
|
|
199
219
|
// Map API status to UI status
|
|
200
220
|
// Pod must be ACTIVE *and* have SSH connection info to be "running"
|
|
201
221
|
let uiStatus: "running" | "stopped" | "starting" = "stopped";
|
|
202
|
-
|
|
222
|
+
const isFullyReady = pod.status === "ACTIVE" && pod.sshConnection;
|
|
223
|
+
|
|
224
|
+
if (isFullyReady) {
|
|
203
225
|
uiStatus = "running";
|
|
204
|
-
} else if (
|
|
226
|
+
} else if (
|
|
227
|
+
pod.status === "ACTIVE" ||
|
|
228
|
+
pod.status === "PROVISIONING" ||
|
|
229
|
+
pod.status === "PENDING"
|
|
230
|
+
) {
|
|
205
231
|
uiStatus = "starting";
|
|
206
232
|
}
|
|
207
233
|
|
|
234
|
+
// Check if this is the pod we're waiting for (provisioning state)
|
|
235
|
+
// and it's now ready - trigger auto-connect
|
|
236
|
+
if (isFullyReady && connectingPodId === pod.id && connectionState === "provisioning") {
|
|
237
|
+
// Schedule auto-connect (give a small delay to avoid race conditions)
|
|
238
|
+
setTimeout(() => handleConnectToPod(pod.id), 1000);
|
|
239
|
+
}
|
|
240
|
+
|
|
208
241
|
return {
|
|
209
242
|
id: pod.id,
|
|
210
243
|
name: pod.name,
|
|
@@ -218,22 +251,54 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
218
251
|
setPods(pods);
|
|
219
252
|
} catch (err) {
|
|
220
253
|
console.error("Failed to load GPU pods:", err);
|
|
254
|
+
const errorMsg = err instanceof Error ? err.message : "Failed to load pods";
|
|
255
|
+
|
|
256
|
+
// If 401 error, API key is invalid - allow user to reset it
|
|
257
|
+
if (errorMsg.includes("401")) {
|
|
258
|
+
setPodsLoadError(`Invalid API key. Please reconfigure ${activeProvider.display_name}.`);
|
|
259
|
+
// Mark provider as needing reconfiguration
|
|
260
|
+
localStorage.removeItem("morecompute_active_provider");
|
|
261
|
+
setSelectedProvider({ ...activeProvider, configured: false });
|
|
262
|
+
} else if (errorMsg.includes("301") || errorMsg.includes("redirect")) {
|
|
263
|
+
setPodsLoadError(`${activeProvider.display_name} API error. Please try again.`);
|
|
264
|
+
} else {
|
|
265
|
+
setPodsLoadError(`Failed to load pods: ${errorMsg}`);
|
|
266
|
+
}
|
|
267
|
+
setPods([]);
|
|
221
268
|
} finally {
|
|
222
269
|
setLoading(false);
|
|
223
270
|
}
|
|
224
271
|
};
|
|
225
272
|
|
|
226
|
-
const loadAvailableGPUs = async () => {
|
|
273
|
+
const loadAvailableGPUs = async (provider?: ProviderInfo | null) => {
|
|
274
|
+
// Use passed provider or fall back to state
|
|
275
|
+
const activeProvider = provider !== undefined ? provider : selectedProvider;
|
|
276
|
+
|
|
277
|
+
// Require a configured provider - don't show GPUs without one
|
|
278
|
+
if (!activeProvider || !activeProvider.configured) {
|
|
279
|
+
setAvailableGPUs([]);
|
|
280
|
+
setGpusLoadError(null);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
227
284
|
setLoadingAvailability(true);
|
|
285
|
+
setGpusLoadError(null);
|
|
228
286
|
try {
|
|
229
|
-
const response = await
|
|
230
|
-
const gpuList: GpuAvailability[] = [];
|
|
231
|
-
Object.values(response).forEach((gpus) => {
|
|
232
|
-
gpuList.push(...gpus);
|
|
233
|
-
});
|
|
287
|
+
const response = await fetchProviderGpuAvailability(activeProvider.name, filters);
|
|
288
|
+
const gpuList: GpuAvailability[] = response.data || [];
|
|
234
289
|
setAvailableGPUs(gpuList);
|
|
235
290
|
} catch (err) {
|
|
236
291
|
console.error("Failed to load GPU availability:", err);
|
|
292
|
+
const errorMsg = err instanceof Error ? err.message : "Failed to load GPUs";
|
|
293
|
+
|
|
294
|
+
if (errorMsg.includes("401")) {
|
|
295
|
+
setGpusLoadError(`Invalid API key. Please reconfigure ${activeProvider.display_name}.`);
|
|
296
|
+
} else if (errorMsg.includes("301") || errorMsg.includes("redirect")) {
|
|
297
|
+
setGpusLoadError(`${activeProvider.display_name} API error. Please try again.`);
|
|
298
|
+
} else {
|
|
299
|
+
setGpusLoadError(`Failed to load GPUs: ${errorMsg}`);
|
|
300
|
+
}
|
|
301
|
+
setAvailableGPUs([]);
|
|
237
302
|
} finally {
|
|
238
303
|
setLoadingAvailability(false);
|
|
239
304
|
}
|
|
@@ -244,6 +309,11 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
244
309
|
setPodCreationError(null);
|
|
245
310
|
|
|
246
311
|
try {
|
|
312
|
+
// Require a configured provider
|
|
313
|
+
if (!selectedProvider || !selectedProvider.configured) {
|
|
314
|
+
throw new Error("Please select and configure a GPU provider first.");
|
|
315
|
+
}
|
|
316
|
+
|
|
247
317
|
// Generate a pod name based on GPU type and timestamp
|
|
248
318
|
const timestamp = new Date()
|
|
249
319
|
.toISOString()
|
|
@@ -271,7 +341,7 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
271
341
|
},
|
|
272
342
|
};
|
|
273
343
|
|
|
274
|
-
const newPod = await
|
|
344
|
+
const newPod = await createProviderPod(selectedProvider.name, podRequest);
|
|
275
345
|
|
|
276
346
|
// Register auto-connect callback for when pod becomes ready
|
|
277
347
|
registerAutoConnect(newPod.id, handleConnectToPod);
|
|
@@ -292,6 +362,8 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
292
362
|
});
|
|
293
363
|
} catch (err) {
|
|
294
364
|
let errorMsg = "Failed to create pod";
|
|
365
|
+
const providerName = selectedProvider?.display_name || "GPU Provider";
|
|
366
|
+
const providerDashboard = selectedProvider?.dashboard_url || "";
|
|
295
367
|
|
|
296
368
|
if (err instanceof Error) {
|
|
297
369
|
errorMsg = err.message;
|
|
@@ -301,11 +373,13 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
301
373
|
errorMsg.includes("402") ||
|
|
302
374
|
errorMsg.includes("Insufficient funds")
|
|
303
375
|
) {
|
|
304
|
-
errorMsg =
|
|
305
|
-
"Insufficient funds. Please add credits to your Prime Intellect wallet:\nhttps://app.primeintellect.ai/dashboard/billing";
|
|
376
|
+
errorMsg = `Insufficient funds. Please add credits to your ${providerName} account.${providerDashboard ? `\n${providerDashboard}` : ""}`;
|
|
306
377
|
} else if (errorMsg.includes("401") || errorMsg.includes("403")) {
|
|
307
|
-
errorMsg =
|
|
308
|
-
} else if (
|
|
378
|
+
errorMsg = `Authentication failed. Please check your ${providerName} API key configuration.`;
|
|
379
|
+
} else if (
|
|
380
|
+
errorMsg.includes("503") ||
|
|
381
|
+
errorMsg.includes("not available")
|
|
382
|
+
) {
|
|
309
383
|
errorMsg =
|
|
310
384
|
"This GPU type is currently unavailable. Please try selecting a different GPU from the list below.";
|
|
311
385
|
} else if (errorMsg.includes("data_center_id")) {
|
|
@@ -317,13 +391,13 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
317
391
|
setPodCreationError(errorMsg);
|
|
318
392
|
|
|
319
393
|
// Show error in modal with link to billing if insufficient funds
|
|
320
|
-
if (errorMsg.includes("Insufficient funds")) {
|
|
394
|
+
if (errorMsg.includes("Insufficient funds") && providerDashboard) {
|
|
321
395
|
setErrorModal({
|
|
322
396
|
isOpen: true,
|
|
323
397
|
title: "Insufficient Funds",
|
|
324
398
|
message: errorMsg,
|
|
325
399
|
actionLabel: "Add Credits",
|
|
326
|
-
actionUrl:
|
|
400
|
+
actionUrl: providerDashboard,
|
|
327
401
|
});
|
|
328
402
|
} else {
|
|
329
403
|
setErrorModal({
|
|
@@ -338,9 +412,12 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
338
412
|
};
|
|
339
413
|
|
|
340
414
|
const handleConnectToPod = async (podId: string) => {
|
|
341
|
-
// Prevent double-connecting
|
|
342
|
-
if (
|
|
343
|
-
|
|
415
|
+
// Prevent double-connecting - but allow transition from "provisioning" to "deploying"
|
|
416
|
+
if (connectedPodId === podId) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
// Only skip if we're already in "deploying" state (actually connecting)
|
|
420
|
+
if (connectingPodId === podId && connectionState === "deploying") {
|
|
344
421
|
return;
|
|
345
422
|
}
|
|
346
423
|
|
|
@@ -348,11 +425,15 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
348
425
|
setConnectionState("deploying"); // Show "Deploying worker..." banner
|
|
349
426
|
setConnectionHealth("unknown"); // Reset health status during connection
|
|
350
427
|
try {
|
|
428
|
+
// Require a configured provider
|
|
429
|
+
if (!selectedProvider || !selectedProvider.configured) {
|
|
430
|
+
throw new Error("Please select and configure a GPU provider first.");
|
|
431
|
+
}
|
|
432
|
+
|
|
351
433
|
// Initiate connection (now returns immediately with "connecting" status)
|
|
352
|
-
const result = await
|
|
434
|
+
const result = await connectToProviderPod(selectedProvider.name, podId);
|
|
353
435
|
|
|
354
436
|
if (result.status === "connecting") {
|
|
355
|
-
console.log("[CONNECT] Connection initiated, polling for completion...");
|
|
356
437
|
|
|
357
438
|
// Poll connection status until it's connected or fails
|
|
358
439
|
const maxAttempts = 30; // 30 seconds max
|
|
@@ -364,7 +445,24 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
364
445
|
attempts++;
|
|
365
446
|
|
|
366
447
|
try {
|
|
367
|
-
|
|
448
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
449
|
+
const status: PodConnectionStatus & { error?: string } = await getPodConnectionStatus() as any;
|
|
450
|
+
|
|
451
|
+
// Check for connection error from backend
|
|
452
|
+
if (status.error && !isComplete) {
|
|
453
|
+
isComplete = true;
|
|
454
|
+
clearInterval(pollInterval);
|
|
455
|
+
setConnectionHealth("unhealthy");
|
|
456
|
+
setConnectingPodId(null);
|
|
457
|
+
setConnectionState(null); // Hide banner on error
|
|
458
|
+
|
|
459
|
+
setErrorModal({
|
|
460
|
+
isOpen: true,
|
|
461
|
+
title: "Connection Failed",
|
|
462
|
+
message: status.error,
|
|
463
|
+
});
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
368
466
|
|
|
369
467
|
if (status.connected && status.pod?.id === podId && !isComplete) {
|
|
370
468
|
// Successfully connected!
|
|
@@ -380,8 +478,9 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
380
478
|
|
|
381
479
|
setErrorModal({
|
|
382
480
|
isOpen: true,
|
|
383
|
-
title: "
|
|
384
|
-
message:
|
|
481
|
+
title: "Connected!",
|
|
482
|
+
message:
|
|
483
|
+
"Successfully connected to GPU pod. You can now run code on the remote GPU.",
|
|
385
484
|
});
|
|
386
485
|
|
|
387
486
|
// Hide the connected banner after 3 seconds
|
|
@@ -398,7 +497,8 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
398
497
|
setErrorModal({
|
|
399
498
|
isOpen: true,
|
|
400
499
|
title: "Connection Timeout",
|
|
401
|
-
message:
|
|
500
|
+
message:
|
|
501
|
+
"Connection took too long. The pod may not be ready yet. Check the pod status and try again.",
|
|
402
502
|
});
|
|
403
503
|
}
|
|
404
504
|
} catch (err) {
|
|
@@ -415,7 +515,6 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
415
515
|
});
|
|
416
516
|
}
|
|
417
517
|
}, 1000); // Poll every second
|
|
418
|
-
|
|
419
518
|
} else if (result.status === "ok") {
|
|
420
519
|
// Immediate success (backwards compatible)
|
|
421
520
|
setConnectedPodId(podId);
|
|
@@ -428,7 +527,8 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
428
527
|
setErrorModal({
|
|
429
528
|
isOpen: true,
|
|
430
529
|
title: "✓ Connected!",
|
|
431
|
-
message:
|
|
530
|
+
message:
|
|
531
|
+
"Successfully connected to GPU pod. You can now run code on the remote GPU.",
|
|
432
532
|
});
|
|
433
533
|
|
|
434
534
|
// Hide the connected banner after 3 seconds
|
|
@@ -443,7 +543,10 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
443
543
|
|
|
444
544
|
let errorMsg = result.message || "Connection failed";
|
|
445
545
|
|
|
446
|
-
if (
|
|
546
|
+
if (
|
|
547
|
+
errorMsg.includes("SSH authentication") ||
|
|
548
|
+
errorMsg.includes("SSH public key")
|
|
549
|
+
) {
|
|
447
550
|
setErrorModal({
|
|
448
551
|
isOpen: true,
|
|
449
552
|
title: "SSH Key Required",
|
|
@@ -460,7 +563,8 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
460
563
|
}
|
|
461
564
|
}
|
|
462
565
|
} catch (err) {
|
|
463
|
-
const errorMsg =
|
|
566
|
+
const errorMsg =
|
|
567
|
+
err instanceof Error ? err.message : "Failed to connect to pod";
|
|
464
568
|
setConnectionHealth("unhealthy");
|
|
465
569
|
setConnectingPodId(null);
|
|
466
570
|
setConnectionState(null); // Hide banner on error
|
|
@@ -485,7 +589,8 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
485
589
|
setErrorModal({
|
|
486
590
|
isOpen: true,
|
|
487
591
|
title: "⚠️ Disconnected",
|
|
488
|
-
message:
|
|
592
|
+
message:
|
|
593
|
+
"Disconnected from pod. Note: The pod is still running and incurring costs. You can reconnect or terminate it below.",
|
|
489
594
|
});
|
|
490
595
|
} catch (err) {
|
|
491
596
|
const errorMsg =
|
|
@@ -517,7 +622,12 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
517
622
|
setConnectionState(null);
|
|
518
623
|
}
|
|
519
624
|
|
|
520
|
-
|
|
625
|
+
// Require a configured provider
|
|
626
|
+
if (!selectedProvider || !selectedProvider.configured) {
|
|
627
|
+
throw new Error("Please select and configure a GPU provider first.");
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
await deleteProviderPod(selectedProvider.name, podId);
|
|
521
631
|
|
|
522
632
|
// Clear discovered pod if it was the one we just deleted
|
|
523
633
|
if (discoveredPod && discoveredPod.id === podId) {
|
|
@@ -545,8 +655,39 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
545
655
|
});
|
|
546
656
|
};
|
|
547
657
|
|
|
548
|
-
const
|
|
549
|
-
|
|
658
|
+
const handleOpenProviderDashboard = () => {
|
|
659
|
+
if (selectedProvider?.dashboard_url) {
|
|
660
|
+
window.open(selectedProvider.dashboard_url, "_blank");
|
|
661
|
+
}
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
// Handle provider selection
|
|
665
|
+
const handleProviderSelect = (provider: ProviderInfo) => {
|
|
666
|
+
if (!provider.configured) {
|
|
667
|
+
// Open config modal for unconfigured provider
|
|
668
|
+
setProviderToConfig(provider);
|
|
669
|
+
setShowProviderConfig(true);
|
|
670
|
+
} else {
|
|
671
|
+
// Use the configured provider
|
|
672
|
+
setSelectedProvider(provider);
|
|
673
|
+
// Clear filters when switching providers (provider-specific filters won't apply)
|
|
674
|
+
setFilters({});
|
|
675
|
+
// Reload data from this provider
|
|
676
|
+
loadGPUPods(undefined, provider);
|
|
677
|
+
loadAvailableGPUs(provider);
|
|
678
|
+
}
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
// Handle provider configuration complete
|
|
682
|
+
const handleProviderConfigured = (provider: ProviderInfo) => {
|
|
683
|
+
setSelectedProvider(provider);
|
|
684
|
+
setShowProviderConfig(false);
|
|
685
|
+
setProviderToConfig(null);
|
|
686
|
+
// Clear filters when switching providers
|
|
687
|
+
setFilters({});
|
|
688
|
+
// Load data from newly configured provider
|
|
689
|
+
loadGPUPods(undefined, provider);
|
|
690
|
+
loadAvailableGPUs(provider);
|
|
550
691
|
};
|
|
551
692
|
|
|
552
693
|
const handleSaveApiKey = async () => {
|
|
@@ -561,8 +702,10 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
561
702
|
try {
|
|
562
703
|
await setGpuApiKey(apiKey);
|
|
563
704
|
setApiConfigured(true);
|
|
705
|
+
setShowApiKeyInput(false);
|
|
564
706
|
setApiKey("");
|
|
565
707
|
await loadGPUPods();
|
|
708
|
+
await loadAvailableGPUs();
|
|
566
709
|
} catch (err) {
|
|
567
710
|
setSaveError(
|
|
568
711
|
err instanceof Error ? err.message : "Failed to save API key",
|
|
@@ -611,12 +754,39 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
611
754
|
cancelLabel="Cancel"
|
|
612
755
|
isDangerous={true}
|
|
613
756
|
/>
|
|
757
|
+
{showProviderConfig && providerToConfig && (
|
|
758
|
+
<ProviderConfigModal
|
|
759
|
+
provider={providerToConfig}
|
|
760
|
+
onClose={() => {
|
|
761
|
+
setShowProviderConfig(false);
|
|
762
|
+
setProviderToConfig(null);
|
|
763
|
+
}}
|
|
764
|
+
onConfigured={handleProviderConfigured}
|
|
765
|
+
/>
|
|
766
|
+
)}
|
|
614
767
|
<div className="runtime-popup">
|
|
768
|
+
{/* Provider Selector */}
|
|
769
|
+
<section className="runtime-section" style={{ padding: "12px 16px" }}>
|
|
770
|
+
<ProviderDropdown
|
|
771
|
+
selectedProvider={selectedProvider}
|
|
772
|
+
onProviderChange={handleProviderSelect}
|
|
773
|
+
onConfigureProvider={(provider) => {
|
|
774
|
+
setProviderToConfig(provider);
|
|
775
|
+
setShowProviderConfig(true);
|
|
776
|
+
}}
|
|
777
|
+
/>
|
|
778
|
+
</section>
|
|
779
|
+
|
|
780
|
+
{/* Divider */}
|
|
781
|
+
<div
|
|
782
|
+
style={{
|
|
783
|
+
height: "1px",
|
|
784
|
+
backgroundColor: "rgba(128, 128, 128, 0.2)",
|
|
785
|
+
margin: "0 16px",
|
|
786
|
+
}}
|
|
787
|
+
/>
|
|
615
788
|
{/* Kernel Status Section */}
|
|
616
|
-
<section
|
|
617
|
-
className="runtime-section"
|
|
618
|
-
style={{ padding: "16px 20px" }}
|
|
619
|
-
>
|
|
789
|
+
<section className="runtime-section" style={{ padding: "16px 20px" }}>
|
|
620
790
|
<div
|
|
621
791
|
style={{
|
|
622
792
|
display: "flex",
|
|
@@ -625,7 +795,13 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
625
795
|
}}
|
|
626
796
|
>
|
|
627
797
|
<div>
|
|
628
|
-
<div
|
|
798
|
+
<div
|
|
799
|
+
style={{
|
|
800
|
+
fontSize: "11px",
|
|
801
|
+
color: "var(--text-secondary)",
|
|
802
|
+
marginBottom: "4px",
|
|
803
|
+
}}
|
|
804
|
+
>
|
|
629
805
|
The kernel is currently: {kernelStatus ? "Running" : "Stopped"}
|
|
630
806
|
</div>
|
|
631
807
|
</div>
|
|
@@ -633,11 +809,13 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
633
809
|
</section>
|
|
634
810
|
|
|
635
811
|
{/* Divider */}
|
|
636
|
-
<div
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
812
|
+
<div
|
|
813
|
+
style={{
|
|
814
|
+
height: "1px",
|
|
815
|
+
backgroundColor: "rgba(128, 128, 128, 0.2)",
|
|
816
|
+
margin: "0 16px",
|
|
817
|
+
}}
|
|
818
|
+
/>
|
|
641
819
|
|
|
642
820
|
{/* Compute Profile Section */}
|
|
643
821
|
<section className="runtime-section" style={{ padding: "12px 16px" }}>
|
|
@@ -648,13 +826,24 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
648
826
|
alignItems: "center",
|
|
649
827
|
}}
|
|
650
828
|
>
|
|
651
|
-
<h3
|
|
829
|
+
<h3
|
|
830
|
+
className="runtime-section-title"
|
|
831
|
+
style={{ fontSize: "12px", fontWeight: 500 }}
|
|
832
|
+
>
|
|
652
833
|
Compute Profile
|
|
653
834
|
</h3>
|
|
654
|
-
<span
|
|
835
|
+
<span
|
|
836
|
+
className="runtime-cost"
|
|
837
|
+
style={{ fontSize: "12px", fontWeight: 500 }}
|
|
838
|
+
>
|
|
655
839
|
{(() => {
|
|
656
|
-
const runningPods = gpuPods.filter(
|
|
657
|
-
|
|
840
|
+
const runningPods = gpuPods.filter(
|
|
841
|
+
(p) => p.status === "running",
|
|
842
|
+
);
|
|
843
|
+
const totalCost = runningPods.reduce(
|
|
844
|
+
(sum, pod) => sum + pod.costPerHour,
|
|
845
|
+
0,
|
|
846
|
+
);
|
|
658
847
|
return `$${totalCost.toFixed(2)} / hour`;
|
|
659
848
|
})()}
|
|
660
849
|
</span>
|
|
@@ -662,399 +851,523 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
662
851
|
</section>
|
|
663
852
|
|
|
664
853
|
{/* Divider */}
|
|
665
|
-
<div
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
854
|
+
<div
|
|
855
|
+
style={{
|
|
856
|
+
height: "1px",
|
|
857
|
+
backgroundColor: "rgba(128, 128, 128, 0.2)",
|
|
858
|
+
margin: "0 16px",
|
|
859
|
+
}}
|
|
860
|
+
/>
|
|
861
|
+
|
|
862
|
+
{/* GPU Pods Section */}
|
|
863
|
+
<section className="runtime-section" style={{ padding: "12px 16px" }}>
|
|
864
|
+
<div
|
|
865
|
+
style={{
|
|
866
|
+
display: "flex",
|
|
867
|
+
justifyContent: "space-between",
|
|
868
|
+
alignItems: "center",
|
|
869
|
+
marginBottom: "12px",
|
|
870
|
+
}}
|
|
871
|
+
>
|
|
872
|
+
<h3
|
|
873
|
+
className="runtime-section-title"
|
|
874
|
+
style={{ fontSize: "12px", fontWeight: 500 }}
|
|
680
875
|
>
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
876
|
+
Remote GPU Pods
|
|
877
|
+
</h3>
|
|
878
|
+
{selectedProvider && (
|
|
879
|
+
<button
|
|
880
|
+
className="runtime-btn runtime-btn-secondary"
|
|
881
|
+
onClick={handleOpenProviderDashboard}
|
|
882
|
+
style={{
|
|
883
|
+
fontSize: "11px",
|
|
884
|
+
padding: "6px 12px",
|
|
885
|
+
backgroundColor: "var(--mc-text-color)",
|
|
886
|
+
color: "var(--mc-cell-background)",
|
|
887
|
+
border: "none",
|
|
888
|
+
borderRadius: "8px",
|
|
889
|
+
cursor: "pointer",
|
|
890
|
+
}}
|
|
891
|
+
>
|
|
892
|
+
Manage
|
|
893
|
+
</button>
|
|
894
|
+
)}
|
|
895
|
+
</div>
|
|
896
|
+
|
|
897
|
+
{apiConfigured === false || showApiKeyInput ? (
|
|
898
|
+
<div className="runtime-empty-state" style={{ padding: "6px" }}>
|
|
899
|
+
<p
|
|
900
|
+
style={{
|
|
901
|
+
marginBottom: "4px",
|
|
902
|
+
color: "var(--text-secondary)",
|
|
903
|
+
fontSize: "10px",
|
|
904
|
+
}}
|
|
905
|
+
>
|
|
906
|
+
Enter API key to enable GPU pods
|
|
907
|
+
</p>
|
|
908
|
+
<div style={{ marginBottom: "4px", width: "100%" }}>
|
|
909
|
+
<input
|
|
910
|
+
type="password"
|
|
911
|
+
placeholder="API key"
|
|
912
|
+
value={apiKey}
|
|
913
|
+
onChange={(e) => setApiKey(e.target.value)}
|
|
914
|
+
onKeyPress={(e) => e.key === "Enter" && handleSaveApiKey()}
|
|
915
|
+
style={{
|
|
916
|
+
width: "100%",
|
|
917
|
+
padding: "6px 12px",
|
|
918
|
+
borderRadius: "8px",
|
|
919
|
+
border: "1px solid var(--border-color)",
|
|
920
|
+
backgroundColor: "var(--background)",
|
|
921
|
+
color: "var(--text)",
|
|
922
|
+
fontSize: "11px",
|
|
923
|
+
marginBottom: "3px",
|
|
924
|
+
}}
|
|
925
|
+
/>
|
|
926
|
+
{saveError && (
|
|
927
|
+
<p
|
|
928
|
+
style={{
|
|
929
|
+
color: "var(--error-color)",
|
|
930
|
+
fontSize: "10px",
|
|
931
|
+
marginBottom: "4px",
|
|
932
|
+
}}
|
|
933
|
+
>
|
|
934
|
+
{saveError}
|
|
935
|
+
</p>
|
|
936
|
+
)}
|
|
937
|
+
</div>
|
|
938
|
+
<div style={{ display: "flex", gap: "4px", width: "100%" }}>
|
|
939
|
+
<button
|
|
940
|
+
className="runtime-btn runtime-btn-primary"
|
|
941
|
+
onClick={handleSaveApiKey}
|
|
942
|
+
disabled={saving}
|
|
943
|
+
style={{
|
|
944
|
+
flex: 1,
|
|
945
|
+
fontSize: "11px",
|
|
946
|
+
padding: "6px 12px",
|
|
947
|
+
backgroundColor: "var(--mc-text-color)",
|
|
948
|
+
color: "var(--mc-cell-background)",
|
|
949
|
+
border: "none",
|
|
950
|
+
borderRadius: "8px",
|
|
951
|
+
cursor: "pointer",
|
|
952
|
+
}}
|
|
953
|
+
>
|
|
954
|
+
{saving ? "Saving..." : "Save"}
|
|
955
|
+
</button>
|
|
685
956
|
<button
|
|
686
957
|
className="runtime-btn runtime-btn-secondary"
|
|
687
|
-
onClick={
|
|
958
|
+
onClick={handleOpenProviderDashboard}
|
|
688
959
|
style={{
|
|
689
960
|
fontSize: "11px",
|
|
690
961
|
padding: "6px 12px",
|
|
691
|
-
backgroundColor: "
|
|
692
|
-
color: "
|
|
962
|
+
backgroundColor: "var(--mc-text-color)",
|
|
963
|
+
color: "var(--mc-cell-background)",
|
|
693
964
|
border: "none",
|
|
694
965
|
borderRadius: "8px",
|
|
695
|
-
cursor: "pointer"
|
|
966
|
+
cursor: "pointer",
|
|
696
967
|
}}
|
|
697
968
|
>
|
|
698
|
-
|
|
969
|
+
<ExternalLink size={10} style={{ marginRight: "3px" }} />
|
|
970
|
+
Get Key
|
|
699
971
|
</button>
|
|
700
|
-
|
|
972
|
+
</div>
|
|
701
973
|
</div>
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
974
|
+
) : loading || apiConfigured === null ? (
|
|
975
|
+
<div
|
|
976
|
+
style={{
|
|
977
|
+
padding: "8px 0",
|
|
978
|
+
color: "var(--text-secondary)",
|
|
979
|
+
fontSize: "11px",
|
|
980
|
+
}}
|
|
981
|
+
>
|
|
982
|
+
Loading...
|
|
983
|
+
</div>
|
|
984
|
+
) : podsLoadError ? (
|
|
985
|
+
<div
|
|
986
|
+
style={{
|
|
987
|
+
padding: "8px 12px",
|
|
988
|
+
backgroundColor: "rgba(239, 68, 68, 0.1)",
|
|
989
|
+
border: "1px solid rgba(239, 68, 68, 0.3)",
|
|
990
|
+
borderRadius: "6px",
|
|
991
|
+
color: "#ef4444",
|
|
992
|
+
fontSize: "11px",
|
|
993
|
+
}}
|
|
994
|
+
>
|
|
995
|
+
{podsLoadError}
|
|
996
|
+
</div>
|
|
997
|
+
) : gpuPods.filter((p) => p.status === "running").length === 0 &&
|
|
998
|
+
!discoveredPod ? (
|
|
999
|
+
<div
|
|
1000
|
+
style={{
|
|
1001
|
+
padding: "8px 0",
|
|
1002
|
+
color: "var(--text-secondary)",
|
|
1003
|
+
fontSize: "11px",
|
|
1004
|
+
}}
|
|
1005
|
+
>
|
|
1006
|
+
Currently not connected to any.
|
|
1007
|
+
</div>
|
|
1008
|
+
) : !connectedPodId && discoveredPod ? (
|
|
1009
|
+
<div style={{ padding: "8px 0" }}>
|
|
1010
|
+
<div
|
|
1011
|
+
style={{
|
|
1012
|
+
backgroundColor: "rgba(251, 191, 36, 0.1)",
|
|
1013
|
+
border: "1px solid rgba(251, 191, 36, 0.3)",
|
|
1014
|
+
borderRadius: "8px",
|
|
1015
|
+
padding: "12px",
|
|
1016
|
+
marginBottom: "8px",
|
|
1017
|
+
}}
|
|
1018
|
+
>
|
|
1019
|
+
<div
|
|
706
1020
|
style={{
|
|
1021
|
+
fontSize: "11px",
|
|
1022
|
+
fontWeight: 600,
|
|
1023
|
+
color: "var(--mc-text-color)",
|
|
707
1024
|
marginBottom: "4px",
|
|
1025
|
+
}}
|
|
1026
|
+
>
|
|
1027
|
+
⚠️ Running Pod Detected
|
|
1028
|
+
</div>
|
|
1029
|
+
<div
|
|
1030
|
+
style={{
|
|
1031
|
+
fontSize: "10px",
|
|
708
1032
|
color: "var(--text-secondary)",
|
|
1033
|
+
marginBottom: "8px",
|
|
1034
|
+
}}
|
|
1035
|
+
>
|
|
1036
|
+
Found a running pod but not connected (backend may have
|
|
1037
|
+
restarted). This pod is still costing money!
|
|
1038
|
+
</div>
|
|
1039
|
+
<div
|
|
1040
|
+
style={{
|
|
709
1041
|
fontSize: "10px",
|
|
1042
|
+
marginBottom: "8px",
|
|
1043
|
+
color: "var(--mc-text-color)",
|
|
710
1044
|
}}
|
|
711
1045
|
>
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
<input
|
|
716
|
-
type="password"
|
|
717
|
-
placeholder="API key"
|
|
718
|
-
value={apiKey}
|
|
719
|
-
onChange={(e) => setApiKey(e.target.value)}
|
|
720
|
-
onKeyPress={(e) => e.key === "Enter" && handleSaveApiKey()}
|
|
721
|
-
style={{
|
|
722
|
-
width: "100%",
|
|
723
|
-
padding: "6px 12px",
|
|
724
|
-
borderRadius: "8px",
|
|
725
|
-
border: "1px solid var(--border-color)",
|
|
726
|
-
backgroundColor: "var(--background)",
|
|
727
|
-
color: "var(--text)",
|
|
728
|
-
fontSize: "11px",
|
|
729
|
-
marginBottom: "3px",
|
|
730
|
-
}}
|
|
731
|
-
/>
|
|
732
|
-
{saveError && (
|
|
733
|
-
<p
|
|
734
|
-
style={{
|
|
735
|
-
color: "var(--error-color)",
|
|
736
|
-
fontSize: "10px",
|
|
737
|
-
marginBottom: "4px",
|
|
738
|
-
}}
|
|
739
|
-
>
|
|
740
|
-
{saveError}
|
|
741
|
-
</p>
|
|
742
|
-
)}
|
|
1046
|
+
<span style={{ fontWeight: 500 }}>{discoveredPod.name}</span>{" "}
|
|
1047
|
+
• {discoveredPod.gpuType} • $
|
|
1048
|
+
{discoveredPod.costPerHour.toFixed(2)}/hour
|
|
743
1049
|
</div>
|
|
744
|
-
<div style={{ display: "flex", gap: "
|
|
1050
|
+
<div style={{ display: "flex", gap: "6px" }}>
|
|
745
1051
|
<button
|
|
746
|
-
className="runtime-btn runtime-btn-
|
|
747
|
-
onClick={
|
|
748
|
-
disabled={saving}
|
|
1052
|
+
className="runtime-btn runtime-btn-secondary"
|
|
1053
|
+
onClick={() => handleConnectToPod(discoveredPod.id)}
|
|
749
1054
|
style={{
|
|
750
1055
|
flex: 1,
|
|
751
|
-
fontSize: "
|
|
1056
|
+
fontSize: "10px",
|
|
752
1057
|
padding: "6px 12px",
|
|
753
|
-
backgroundColor: "
|
|
754
|
-
color: "
|
|
1058
|
+
backgroundColor: "rgba(251, 191, 36, 0.8)",
|
|
1059
|
+
color: "var(--mc-text-color)",
|
|
755
1060
|
border: "none",
|
|
756
1061
|
borderRadius: "8px",
|
|
757
|
-
cursor: "pointer"
|
|
1062
|
+
cursor: "pointer",
|
|
1063
|
+
fontWeight: 600,
|
|
758
1064
|
}}
|
|
759
1065
|
>
|
|
760
|
-
|
|
1066
|
+
Reconnect
|
|
761
1067
|
</button>
|
|
762
1068
|
<button
|
|
763
1069
|
className="runtime-btn runtime-btn-secondary"
|
|
764
|
-
onClick={
|
|
1070
|
+
onClick={() =>
|
|
1071
|
+
handleDeletePod(discoveredPod.id, discoveredPod.name)
|
|
1072
|
+
}
|
|
1073
|
+
disabled={deletingPodId === discoveredPod.id}
|
|
765
1074
|
style={{
|
|
766
|
-
fontSize: "
|
|
1075
|
+
fontSize: "10px",
|
|
767
1076
|
padding: "6px 12px",
|
|
768
|
-
backgroundColor: "
|
|
1077
|
+
backgroundColor: "rgba(220, 38, 38, 0.9)",
|
|
769
1078
|
color: "white",
|
|
770
1079
|
border: "none",
|
|
771
1080
|
borderRadius: "8px",
|
|
772
|
-
cursor: "pointer"
|
|
1081
|
+
cursor: "pointer",
|
|
773
1082
|
}}
|
|
774
1083
|
>
|
|
775
|
-
|
|
776
|
-
Get Key
|
|
1084
|
+
{deletingPodId === discoveredPod.id ? "..." : "Terminate"}
|
|
777
1085
|
</button>
|
|
778
1086
|
</div>
|
|
779
1087
|
</div>
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
<div style={{ padding: "8px 0" }}>
|
|
790
|
-
<div style={{
|
|
791
|
-
backgroundColor: "#fff3cd",
|
|
792
|
-
border: "1px solid #ffc107",
|
|
793
|
-
borderRadius: "8px",
|
|
794
|
-
padding: "12px",
|
|
795
|
-
marginBottom: "8px"
|
|
796
|
-
}}>
|
|
797
|
-
<div style={{ fontSize: "11px", fontWeight: 600, color: "#856404", marginBottom: "4px" }}>
|
|
798
|
-
⚠️ Running Pod Detected
|
|
799
|
-
</div>
|
|
800
|
-
<div style={{ fontSize: "10px", color: "#856404", marginBottom: "8px" }}>
|
|
801
|
-
Found a running pod but not connected (backend may have restarted). This pod is still costing money!
|
|
802
|
-
</div>
|
|
803
|
-
<div style={{ fontSize: "10px", marginBottom: "8px" }}>
|
|
804
|
-
<span style={{ fontWeight: 500 }}>{discoveredPod.name}</span> • {discoveredPod.gpuType} • ${discoveredPod.costPerHour.toFixed(2)}/hour
|
|
805
|
-
</div>
|
|
806
|
-
<div style={{ display: "flex", gap: "6px" }}>
|
|
807
|
-
<button
|
|
808
|
-
className="runtime-btn runtime-btn-secondary"
|
|
809
|
-
onClick={() => handleConnectToPod(discoveredPod.id)}
|
|
810
|
-
style={{
|
|
811
|
-
flex: 1,
|
|
812
|
-
fontSize: "10px",
|
|
813
|
-
padding: "6px 12px",
|
|
814
|
-
backgroundColor: "#ffc107",
|
|
815
|
-
color: "#000",
|
|
816
|
-
border: "none",
|
|
817
|
-
borderRadius: "8px",
|
|
818
|
-
cursor: "pointer",
|
|
819
|
-
fontWeight: 600
|
|
820
|
-
}}
|
|
821
|
-
>
|
|
822
|
-
Reconnect
|
|
823
|
-
</button>
|
|
824
|
-
<button
|
|
825
|
-
className="runtime-btn runtime-btn-secondary"
|
|
826
|
-
onClick={() => handleDeletePod(discoveredPod.id, discoveredPod.name)}
|
|
827
|
-
disabled={deletingPodId === discoveredPod.id}
|
|
1088
|
+
</div>
|
|
1089
|
+
) : (
|
|
1090
|
+
gpuPods
|
|
1091
|
+
.filter((pod) => pod.status === "running")
|
|
1092
|
+
.map((pod) => {
|
|
1093
|
+
const isConnected = pod.id === connectedPodId;
|
|
1094
|
+
return (
|
|
1095
|
+
<div key={pod.id} style={{ padding: "8px 0" }}>
|
|
1096
|
+
<div
|
|
828
1097
|
style={{
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
border: "none",
|
|
834
|
-
borderRadius: "8px",
|
|
835
|
-
cursor: "pointer"
|
|
1098
|
+
display: "flex",
|
|
1099
|
+
justifyContent: "space-between",
|
|
1100
|
+
alignItems: "flex-start",
|
|
1101
|
+
marginBottom: "8px",
|
|
836
1102
|
}}
|
|
837
1103
|
>
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
gpuPods
|
|
845
|
-
.filter((pod) => pod.status === "running")
|
|
846
|
-
.map((pod) => {
|
|
847
|
-
const isConnected = pod.id === connectedPodId;
|
|
848
|
-
return (
|
|
849
|
-
<div key={pod.id} style={{ padding: "8px 0" }}>
|
|
850
|
-
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", marginBottom: "8px" }}>
|
|
851
|
-
<div>
|
|
852
|
-
<div style={{ fontSize: "11px", marginBottom: "4px" }}>
|
|
853
|
-
<span style={{ fontWeight: 500 }}>{pod.name}</span>
|
|
854
|
-
{isConnected && (
|
|
855
|
-
<span style={{
|
|
1104
|
+
<div>
|
|
1105
|
+
<div style={{ fontSize: "11px", marginBottom: "4px" }}>
|
|
1106
|
+
<span style={{ fontWeight: 500 }}>{pod.name}</span>
|
|
1107
|
+
{isConnected && (
|
|
1108
|
+
<span
|
|
1109
|
+
style={{
|
|
856
1110
|
marginLeft: "6px",
|
|
857
1111
|
fontSize: "9px",
|
|
858
|
-
backgroundColor: "
|
|
1112
|
+
backgroundColor: "rgba(16, 185, 129, 0.9)",
|
|
859
1113
|
color: "white",
|
|
860
1114
|
padding: "2px 6px",
|
|
861
|
-
borderRadius: "4px"
|
|
862
|
-
}}
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
</div>
|
|
867
|
-
<div style={{ fontSize: "10px", color: "var(--text-secondary)" }}>
|
|
868
|
-
{pod.gpuType} • ${pod.costPerHour.toFixed(2)}/hour
|
|
869
|
-
</div>
|
|
870
|
-
</div>
|
|
871
|
-
<div style={{ display: "flex", gap: "6px" }}>
|
|
872
|
-
{isConnected ? (
|
|
873
|
-
<>
|
|
874
|
-
<button
|
|
875
|
-
className="runtime-btn runtime-btn-secondary"
|
|
876
|
-
onClick={handleDisconnect}
|
|
877
|
-
style={{
|
|
878
|
-
fontSize: "10px",
|
|
879
|
-
padding: "6px 12px",
|
|
880
|
-
backgroundColor: "#000",
|
|
881
|
-
color: "white",
|
|
882
|
-
border: "none",
|
|
883
|
-
borderRadius: "8px",
|
|
884
|
-
cursor: "pointer"
|
|
885
|
-
}}
|
|
886
|
-
>
|
|
887
|
-
Disconnect
|
|
888
|
-
</button>
|
|
889
|
-
<button
|
|
890
|
-
className="runtime-btn runtime-btn-secondary"
|
|
891
|
-
onClick={() => handleDeletePod(pod.id, pod.name)}
|
|
892
|
-
disabled={deletingPodId === pod.id}
|
|
893
|
-
style={{
|
|
894
|
-
fontSize: "10px",
|
|
895
|
-
padding: "6px 12px",
|
|
896
|
-
backgroundColor: "#dc2626",
|
|
897
|
-
color: "white",
|
|
898
|
-
border: "none",
|
|
899
|
-
borderRadius: "8px",
|
|
900
|
-
cursor: "pointer"
|
|
901
|
-
}}
|
|
902
|
-
>
|
|
903
|
-
{deletingPodId === pod.id ? "..." : "Terminate"}
|
|
904
|
-
</button>
|
|
905
|
-
</>
|
|
906
|
-
) : (
|
|
907
|
-
<>
|
|
908
|
-
<button
|
|
909
|
-
className="runtime-btn runtime-btn-secondary"
|
|
910
|
-
onClick={() => handleConnectToPod(pod.id)}
|
|
911
|
-
disabled={connectingPodId === pod.id}
|
|
912
|
-
style={{
|
|
913
|
-
fontSize: "10px",
|
|
914
|
-
padding: "6px 12px",
|
|
915
|
-
backgroundColor: "#10b981",
|
|
916
|
-
color: "white",
|
|
917
|
-
border: "none",
|
|
918
|
-
borderRadius: "8px",
|
|
919
|
-
cursor: "pointer"
|
|
920
|
-
}}
|
|
921
|
-
>
|
|
922
|
-
{connectingPodId === pod.id ? "Connecting..." : "Connect"}
|
|
923
|
-
</button>
|
|
924
|
-
<button
|
|
925
|
-
className="runtime-btn runtime-btn-secondary"
|
|
926
|
-
onClick={() => handleDeletePod(pod.id, pod.name)}
|
|
927
|
-
disabled={deletingPodId === pod.id}
|
|
928
|
-
style={{
|
|
929
|
-
fontSize: "10px",
|
|
930
|
-
padding: "6px 12px",
|
|
931
|
-
backgroundColor: "#dc2626",
|
|
932
|
-
color: "white",
|
|
933
|
-
border: "none",
|
|
934
|
-
borderRadius: "8px",
|
|
935
|
-
cursor: "pointer"
|
|
936
|
-
}}
|
|
937
|
-
>
|
|
938
|
-
{deletingPodId === pod.id ? "..." : "Terminate"}
|
|
939
|
-
</button>
|
|
940
|
-
</>
|
|
1115
|
+
borderRadius: "4px",
|
|
1116
|
+
}}
|
|
1117
|
+
>
|
|
1118
|
+
Connected
|
|
1119
|
+
</span>
|
|
941
1120
|
)}
|
|
942
1121
|
</div>
|
|
1122
|
+
<div
|
|
1123
|
+
style={{
|
|
1124
|
+
fontSize: "10px",
|
|
1125
|
+
color: "var(--text-secondary)",
|
|
1126
|
+
}}
|
|
1127
|
+
>
|
|
1128
|
+
{pod.gpuType} • ${pod.costPerHour.toFixed(2)}/hour
|
|
1129
|
+
</div>
|
|
1130
|
+
</div>
|
|
1131
|
+
<div style={{ display: "flex", gap: "6px" }}>
|
|
1132
|
+
{isConnected ? (
|
|
1133
|
+
<>
|
|
1134
|
+
<button
|
|
1135
|
+
className="runtime-btn runtime-btn-secondary"
|
|
1136
|
+
onClick={handleDisconnect}
|
|
1137
|
+
style={{
|
|
1138
|
+
fontSize: "10px",
|
|
1139
|
+
padding: "6px 12px",
|
|
1140
|
+
backgroundColor: "var(--mc-text-color)",
|
|
1141
|
+
color: "var(--mc-cell-background)",
|
|
1142
|
+
border: "none",
|
|
1143
|
+
borderRadius: "8px",
|
|
1144
|
+
cursor: "pointer",
|
|
1145
|
+
}}
|
|
1146
|
+
>
|
|
1147
|
+
Disconnect
|
|
1148
|
+
</button>
|
|
1149
|
+
<button
|
|
1150
|
+
className="runtime-btn runtime-btn-secondary"
|
|
1151
|
+
onClick={() => handleDeletePod(pod.id, pod.name)}
|
|
1152
|
+
disabled={deletingPodId === pod.id}
|
|
1153
|
+
style={{
|
|
1154
|
+
fontSize: "10px",
|
|
1155
|
+
padding: "6px 12px",
|
|
1156
|
+
backgroundColor: "rgba(220, 38, 38, 0.9)",
|
|
1157
|
+
color: "white",
|
|
1158
|
+
border: "none",
|
|
1159
|
+
borderRadius: "8px",
|
|
1160
|
+
cursor: "pointer",
|
|
1161
|
+
}}
|
|
1162
|
+
>
|
|
1163
|
+
{deletingPodId === pod.id ? "..." : "Terminate"}
|
|
1164
|
+
</button>
|
|
1165
|
+
</>
|
|
1166
|
+
) : (
|
|
1167
|
+
<>
|
|
1168
|
+
<button
|
|
1169
|
+
className="runtime-btn runtime-btn-secondary"
|
|
1170
|
+
onClick={() => handleConnectToPod(pod.id)}
|
|
1171
|
+
disabled={connectingPodId === pod.id}
|
|
1172
|
+
style={{
|
|
1173
|
+
fontSize: "10px",
|
|
1174
|
+
padding: "6px 12px",
|
|
1175
|
+
backgroundColor: "rgba(16, 185, 129, 0.9)",
|
|
1176
|
+
color: "white",
|
|
1177
|
+
border: "none",
|
|
1178
|
+
borderRadius: "8px",
|
|
1179
|
+
cursor: "pointer",
|
|
1180
|
+
}}
|
|
1181
|
+
>
|
|
1182
|
+
{connectingPodId === pod.id
|
|
1183
|
+
? "Connecting..."
|
|
1184
|
+
: "Connect"}
|
|
1185
|
+
</button>
|
|
1186
|
+
<button
|
|
1187
|
+
className="runtime-btn runtime-btn-secondary"
|
|
1188
|
+
onClick={() => handleDeletePod(pod.id, pod.name)}
|
|
1189
|
+
disabled={deletingPodId === pod.id}
|
|
1190
|
+
style={{
|
|
1191
|
+
fontSize: "10px",
|
|
1192
|
+
padding: "6px 12px",
|
|
1193
|
+
backgroundColor: "rgba(220, 38, 38, 0.9)",
|
|
1194
|
+
color: "white",
|
|
1195
|
+
border: "none",
|
|
1196
|
+
borderRadius: "8px",
|
|
1197
|
+
cursor: "pointer",
|
|
1198
|
+
}}
|
|
1199
|
+
>
|
|
1200
|
+
{deletingPodId === pod.id ? "..." : "Terminate"}
|
|
1201
|
+
</button>
|
|
1202
|
+
</>
|
|
1203
|
+
)}
|
|
943
1204
|
</div>
|
|
944
1205
|
</div>
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
1206
|
+
</div>
|
|
1207
|
+
);
|
|
1208
|
+
})
|
|
1209
|
+
)}
|
|
1210
|
+
</section>
|
|
949
1211
|
|
|
950
1212
|
{/* Divider */}
|
|
951
|
-
<div
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1213
|
+
<div
|
|
1214
|
+
style={{
|
|
1215
|
+
height: "1px",
|
|
1216
|
+
backgroundColor: "rgba(128, 128, 128, 0.2)",
|
|
1217
|
+
margin: "0 16px",
|
|
1218
|
+
}}
|
|
1219
|
+
/>
|
|
1220
|
+
|
|
1221
|
+
{/* Browse Available GPUs Section */}
|
|
1222
|
+
{apiConfigured && (
|
|
1223
|
+
<section className="runtime-section" style={{ padding: "12px 16px" }}>
|
|
1224
|
+
{/* Search and Filter Bar */}
|
|
1225
|
+
<div
|
|
1226
|
+
style={{
|
|
1227
|
+
marginBottom: "20px",
|
|
1228
|
+
display: "flex",
|
|
1229
|
+
gap: "8px",
|
|
1230
|
+
alignItems: "center",
|
|
1231
|
+
}}
|
|
1232
|
+
>
|
|
1233
|
+
<input
|
|
1234
|
+
type="text"
|
|
1235
|
+
placeholder="Search GPU (e.g., H100, A100, RTX 4090)"
|
|
1236
|
+
value={searchQuery}
|
|
1237
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
1238
|
+
style={{
|
|
1239
|
+
flex: 1,
|
|
1240
|
+
padding: "6px 12px",
|
|
1241
|
+
fontSize: "11px",
|
|
1242
|
+
border: "1px solid var(--mc-border)",
|
|
1243
|
+
borderRadius: "8px",
|
|
1244
|
+
backgroundColor: "var(--mc-background)",
|
|
1245
|
+
color: "var(--mc-text-color)",
|
|
1246
|
+
outline: "none",
|
|
1247
|
+
}}
|
|
1248
|
+
/>
|
|
1249
|
+
<button
|
|
1250
|
+
className="runtime-btn runtime-btn-secondary"
|
|
1251
|
+
onClick={() => setShowFilterPopup(!showFilterPopup)}
|
|
1252
|
+
title="Filter GPUs"
|
|
962
1253
|
style={{
|
|
963
|
-
|
|
1254
|
+
padding: "6px 8px",
|
|
1255
|
+
fontSize: "11px",
|
|
1256
|
+
position: "relative",
|
|
1257
|
+
backgroundColor: "var(--mc-text-color)",
|
|
1258
|
+
color: "var(--mc-cell-background)",
|
|
1259
|
+
border: "none",
|
|
1260
|
+
borderRadius: "8px",
|
|
1261
|
+
cursor: "pointer",
|
|
964
1262
|
display: "flex",
|
|
965
|
-
gap: "8px",
|
|
966
1263
|
alignItems: "center",
|
|
1264
|
+
justifyContent: "center",
|
|
967
1265
|
}}
|
|
968
1266
|
>
|
|
969
|
-
<
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
fontSize: "11px",
|
|
991
|
-
position: "relative",
|
|
992
|
-
backgroundColor: "#000",
|
|
993
|
-
color: "white",
|
|
994
|
-
border: "none",
|
|
995
|
-
borderRadius: "8px",
|
|
996
|
-
cursor: "pointer"
|
|
997
|
-
}}
|
|
998
|
-
>
|
|
999
|
-
<Filter size={10} style={{ marginRight: "3px" }} />
|
|
1000
|
-
Filter
|
|
1001
|
-
{(filters.gpu_type ||
|
|
1002
|
-
filters.gpu_count ||
|
|
1003
|
-
filters.security ||
|
|
1004
|
-
filters.socket) && (
|
|
1005
|
-
<span
|
|
1006
|
-
style={{
|
|
1007
|
-
position: "absolute",
|
|
1008
|
-
top: "-2px",
|
|
1009
|
-
right: "-2px",
|
|
1010
|
-
width: "8px",
|
|
1011
|
-
height: "8px",
|
|
1012
|
-
borderRadius: "50%",
|
|
1013
|
-
backgroundColor: "var(--accent)",
|
|
1014
|
-
}}
|
|
1015
|
-
/>
|
|
1016
|
-
)}
|
|
1017
|
-
</button>
|
|
1018
|
-
</div>
|
|
1019
|
-
|
|
1020
|
-
{/* Filter Popup */}
|
|
1021
|
-
<FilterPopup
|
|
1022
|
-
isOpen={showFilterPopup}
|
|
1023
|
-
onClose={() => setShowFilterPopup(false)}
|
|
1024
|
-
filters={filters}
|
|
1025
|
-
onFiltersChange={setFilters}
|
|
1026
|
-
onApply={loadAvailableGPUs}
|
|
1027
|
-
/>
|
|
1267
|
+
<Filter size={14} />
|
|
1268
|
+
{(filters.gpu_type ||
|
|
1269
|
+
filters.gpu_count ||
|
|
1270
|
+
filters.secure_cloud !== undefined ||
|
|
1271
|
+
filters.community_cloud !== undefined ||
|
|
1272
|
+
filters.verified !== undefined ||
|
|
1273
|
+
filters.min_reliability !== undefined) && (
|
|
1274
|
+
<span
|
|
1275
|
+
style={{
|
|
1276
|
+
position: "absolute",
|
|
1277
|
+
top: "-2px",
|
|
1278
|
+
right: "-2px",
|
|
1279
|
+
width: "8px",
|
|
1280
|
+
height: "8px",
|
|
1281
|
+
borderRadius: "50%",
|
|
1282
|
+
backgroundColor: "var(--accent)",
|
|
1283
|
+
}}
|
|
1284
|
+
/>
|
|
1285
|
+
)}
|
|
1286
|
+
</button>
|
|
1287
|
+
</div>
|
|
1028
1288
|
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1289
|
+
{/* Filter Popup */}
|
|
1290
|
+
<FilterPopup
|
|
1291
|
+
isOpen={showFilterPopup}
|
|
1292
|
+
onClose={() => setShowFilterPopup(false)}
|
|
1293
|
+
filters={filters}
|
|
1294
|
+
onFiltersChange={setFilters}
|
|
1295
|
+
onApply={loadAvailableGPUs}
|
|
1296
|
+
providerName={selectedProvider?.name}
|
|
1297
|
+
/>
|
|
1298
|
+
|
|
1299
|
+
{/* Results */}
|
|
1300
|
+
{loadingAvailability ? (
|
|
1301
|
+
<div
|
|
1302
|
+
style={{
|
|
1303
|
+
padding: "16px 0",
|
|
1304
|
+
textAlign: "center",
|
|
1305
|
+
color: "var(--text-secondary)",
|
|
1306
|
+
fontSize: "11px",
|
|
1307
|
+
}}
|
|
1308
|
+
>
|
|
1309
|
+
Loading...
|
|
1310
|
+
</div>
|
|
1311
|
+
) : gpusLoadError ? (
|
|
1312
|
+
<div
|
|
1313
|
+
style={{
|
|
1314
|
+
padding: "12px",
|
|
1315
|
+
backgroundColor: "rgba(239, 68, 68, 0.1)",
|
|
1316
|
+
border: "1px solid rgba(239, 68, 68, 0.3)",
|
|
1317
|
+
borderRadius: "6px",
|
|
1318
|
+
color: "#ef4444",
|
|
1319
|
+
fontSize: "11px",
|
|
1320
|
+
textAlign: "center",
|
|
1321
|
+
}}
|
|
1322
|
+
>
|
|
1323
|
+
{gpusLoadError}
|
|
1324
|
+
</div>
|
|
1325
|
+
) : availableGPUs.length === 0 ? (
|
|
1326
|
+
<div
|
|
1327
|
+
style={{
|
|
1328
|
+
padding: "16px 0",
|
|
1329
|
+
textAlign: "center",
|
|
1330
|
+
color: "var(--text-secondary)",
|
|
1331
|
+
fontSize: "11px",
|
|
1332
|
+
}}
|
|
1333
|
+
>
|
|
1334
|
+
Use filters to find available GPUs
|
|
1335
|
+
</div>
|
|
1336
|
+
) : filteredGPUs.length === 0 ? (
|
|
1337
|
+
<div
|
|
1338
|
+
style={{
|
|
1339
|
+
padding: "16px 0",
|
|
1340
|
+
textAlign: "center",
|
|
1341
|
+
color: "var(--text-secondary)",
|
|
1342
|
+
fontSize: "11px",
|
|
1343
|
+
}}
|
|
1344
|
+
>
|
|
1345
|
+
No GPUs match "{searchQuery}"
|
|
1346
|
+
</div>
|
|
1347
|
+
) : (
|
|
1348
|
+
<div
|
|
1349
|
+
style={{
|
|
1350
|
+
maxHeight: "calc(100vh - 400px)",
|
|
1351
|
+
overflowY: "auto",
|
|
1352
|
+
paddingRight: "12px",
|
|
1353
|
+
}}
|
|
1354
|
+
>
|
|
1355
|
+
{filteredGPUs.map((gpu, index) => (
|
|
1356
|
+
<React.Fragment key={`${gpu.cloudId}-${index}`}>
|
|
1357
|
+
{index > 0 && (
|
|
1053
1358
|
<div
|
|
1054
1359
|
style={{
|
|
1055
|
-
|
|
1360
|
+
height: "1px",
|
|
1361
|
+
backgroundColor: "rgba(128, 128, 128, 0.15)",
|
|
1362
|
+
margin: "8px 0",
|
|
1056
1363
|
}}
|
|
1057
|
-
|
|
1364
|
+
/>
|
|
1365
|
+
)}
|
|
1366
|
+
<div
|
|
1367
|
+
style={{
|
|
1368
|
+
padding: "8px 0",
|
|
1369
|
+
}}
|
|
1370
|
+
>
|
|
1058
1371
|
<div
|
|
1059
1372
|
style={{
|
|
1060
1373
|
display: "flex",
|
|
@@ -1071,7 +1384,7 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
1071
1384
|
marginBottom: "1px",
|
|
1072
1385
|
}}
|
|
1073
1386
|
>
|
|
1074
|
-
{gpu.gpuType} ({gpu.gpuCount}x)
|
|
1387
|
+
{gpu.gpuName || gpu.gpuType} ({gpu.gpuCount}x)
|
|
1075
1388
|
</div>
|
|
1076
1389
|
<div
|
|
1077
1390
|
style={{
|
|
@@ -1079,10 +1392,18 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
1079
1392
|
color: "var(--text-secondary)",
|
|
1080
1393
|
}}
|
|
1081
1394
|
>
|
|
1082
|
-
{gpu.provider}
|
|
1395
|
+
{gpu.provider} • {gpu.memoryGb || gpu.gpuMemory}GB{gpu.regionDescription ? ` • ${gpu.regionDescription}` : ""}
|
|
1083
1396
|
</div>
|
|
1084
1397
|
</div>
|
|
1085
|
-
<div
|
|
1398
|
+
<div
|
|
1399
|
+
style={{
|
|
1400
|
+
textAlign: "right",
|
|
1401
|
+
display: "flex",
|
|
1402
|
+
flexDirection: "column",
|
|
1403
|
+
alignItems: "flex-end",
|
|
1404
|
+
gap: "6px",
|
|
1405
|
+
}}
|
|
1406
|
+
>
|
|
1086
1407
|
<div>
|
|
1087
1408
|
<div
|
|
1088
1409
|
style={{
|
|
@@ -1091,7 +1412,7 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
1091
1412
|
color: "var(--accent)",
|
|
1092
1413
|
}}
|
|
1093
1414
|
>
|
|
1094
|
-
${gpu.prices?.onDemand?.toFixed(2) || "N/A"}/hr
|
|
1415
|
+
${(gpu.priceHr ?? gpu.prices?.onDemand)?.toFixed(2) || "N/A"}/hr
|
|
1095
1416
|
</div>
|
|
1096
1417
|
{gpu.stockStatus && (
|
|
1097
1418
|
<div
|
|
@@ -1119,11 +1440,11 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
1119
1440
|
fontSize: "10px",
|
|
1120
1441
|
padding: "6px 16px",
|
|
1121
1442
|
whiteSpace: "nowrap",
|
|
1122
|
-
backgroundColor: "
|
|
1123
|
-
color: "
|
|
1443
|
+
backgroundColor: "var(--mc-text-color)",
|
|
1444
|
+
color: "var(--mc-cell-background)",
|
|
1124
1445
|
border: "none",
|
|
1125
1446
|
borderRadius: "8px",
|
|
1126
|
-
cursor: "pointer"
|
|
1447
|
+
cursor: "pointer",
|
|
1127
1448
|
}}
|
|
1128
1449
|
>
|
|
1129
1450
|
{creatingPodId === gpu.cloudId
|
|
@@ -1174,12 +1495,12 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
1174
1495
|
)}
|
|
1175
1496
|
</div>
|
|
1176
1497
|
</div>
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1498
|
+
</React.Fragment>
|
|
1499
|
+
))}
|
|
1500
|
+
</div>
|
|
1501
|
+
)}
|
|
1502
|
+
</section>
|
|
1503
|
+
)}
|
|
1183
1504
|
</div>
|
|
1184
1505
|
</>
|
|
1185
1506
|
);
|