diff --git a/deno.lock b/deno.lock index a77c69c9..5253b3e0 100644 --- a/deno.lock +++ b/deno.lock @@ -2,6 +2,7 @@ "version": "5", "specifiers": { "jsr:@std/assert@1.0.11": "1.0.11", + "jsr:@std/fmt@*": "1.0.8", "jsr:@std/internal@^1.0.5": "1.0.12", "npm:@commander-js/extra-typings@^12.1.0": "12.1.0_commander@12.1.0", "npm:@inkjs/ui@2": "2.0.0_ink@5.2.1__@types+react@18.3.27__react@18.3.1_@types+react@18.3.27_react@18.3.1", @@ -13,9 +14,11 @@ "npm:@types/semver@^7.5.8": "7.7.1", "npm:async-retry@^1.3.3": "1.3.3", "npm:axios@^1.8.4": "1.13.2", + "npm:boxen@8.0.1": "8.0.1", "npm:boxen@^8.0.1": "8.0.1", "npm:chrono-node@^2.9.0": "2.9.0", "npm:cli-progress@^3.12.0": "3.12.0", + "npm:cli-spinners@*": "3.3.0", "npm:cli-table3@0.6.5": "0.6.5", "npm:commander@^12.1.0": "12.1.0", "npm:date-fns@^4.1.0": "4.1.0", @@ -39,9 +42,12 @@ "npm:semver@^7.6.3": "7.7.3", "npm:shescape@^2.1.1": "2.1.7", "npm:tiny-invariant@^1.3.3": "1.3.3", + "npm:tweetnacl-util@*": "0.15.1", "npm:tweetnacl-util@~0.15.1": "0.15.1", + "npm:tweetnacl@*": "1.0.3", "npm:tweetnacl@^1.0.3": "1.0.3", - "npm:yaml@2.6.1": "2.6.1" + "npm:yaml@2.6.1": "2.6.1", + "npm:yn@*": "5.1.0" }, "jsr": { "@std/assert@1.0.11": { @@ -50,6 +56,9 @@ "jsr:@std/internal" ] }, + "@std/fmt@1.0.8": { + "integrity": "71e1fc498787e4434d213647a6e43e794af4fd393ef8f52062246e06f7e372b7" + }, "@std/internal@1.0.12": { "integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027" } @@ -107,7 +116,7 @@ "@inquirer/figures", "@inquirer/type@2.0.0", "@types/mute-stream", - "@types/node@22.19.1", + "@types/node@22.19.2", "@types/wrap-ansi", "ansi-escapes@4.3.2", "cli-width", @@ -225,8 +234,8 @@ "@types/node@24.2.0" ] }, - "@types/node@22.19.1": { - "integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==", + "@types/node@22.19.2": { + "integrity": "sha512-LPM2G3Syo1GLzXLGJAKdqoU35XvrWzGJ21/7sgZTUpbkBaOasTj8tjwn6w+hCkqaa1TfJ/w67rJSwYItlJ2mYw==", "dependencies": [ "undici-types@6.21.0" ] @@ -1033,6 +1042,9 @@ "integrity": "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==", "bin": true }, + "yn@5.1.0": { + "integrity": "sha512-TfXLvT6eVsBNIm8rAXTwJYdQFtOXaHQ+rA7LU8HL8C/BFfaSfhvFE5T1rHAdBCbAj808HaqjXVkmo8jmeGOqhw==" + }, "yoctocolors-cjs@2.1.3": { "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==" }, diff --git a/src/lib/nodes/list.tsx b/src/lib/nodes/list.tsx index 815ed5f1..520ea73d 100644 --- a/src/lib/nodes/list.tsx +++ b/src/lib/nodes/list.tsx @@ -1,6 +1,6 @@ import React from "react"; import { Command, Option } from "@commander-js/extra-typings"; -import { brightBlack, gray } from "jsr:@std/fmt/colors"; +import { brightBlack, cyan, gray } from "jsr:@std/fmt/colors"; import console from "node:console"; import ora from "ora"; import dayjs from "dayjs"; @@ -9,6 +9,7 @@ import advanced from "dayjs/plugin/advancedFormat"; import timezone from "dayjs/plugin/timezone"; import { Box, render, Text } from "ink"; import type { SFCNodes } from "@sfcompute/nodes-sdk-alpha"; +import { formatDuration, intervalToDuration } from "date-fns"; import { getAuthToken } from "../../helpers/config.ts"; import { logAndQuit } from "../../helpers/errors.ts"; @@ -51,52 +52,111 @@ function VMTable({ vms }: { vms: NonNullable["data"] }) { return ( - {/* Header */} - - - Virtual Machines - - - Status - - - Start/End - - - - {/* VM rows */} - {vmsToShow.map((vm) => { - const startDate = vm.start_at ? dayjs.unix(vm.start_at) : null; - const endDate = vm.end_at ? dayjs.unix(vm.end_at) : null; - const startEnd = formatNullableDateRange(startDate, endDate); - - return ( - - + {/* Build table as columns */} + + {/* Column 1: VM IDs */} + + + Previous VMs + + {vmsToShow.map((vm) => ( + {vm.id} - + ))} + + + {/* Column 2: Status */} + + + Status + + {vmsToShow.map((vm) => ( + {getVMStatusColor(vm.status)} - - {startEnd} + ))} + + + {/* Column 3: Zone */} + + + Zone + + {vmsToShow.map((vm) => ( + + {vm.zone} + ))} + + + {/* Column 4: Start/End */} + + + Start/End - ); - })} + {vmsToShow.map((vm) => { + const startDate = vm.start_at ? dayjs.unix(vm.start_at) : null; + const endDate = vm.end_at ? dayjs.unix(vm.end_at) : null; + const startEnd = formatNullableDateRange(startDate, endDate); + return ( + + {startEnd} + + ); + })} + + {/* Show message if there are more VMs */} {remainingVms > 0 && ( - + {remainingVms} past {remainingVms === 1 ? "VM" : "VMs"} not shown. @@ -268,12 +328,24 @@ function getActionsForNode(node: SFCNodes.Node) { // Component for displaying a single node in verbose format function NodeVerboseDisplay({ node }: { node: SFCNodes.Node }) { // Convert Unix timestamps to dates and calculate duration - const startDate = node.start_at && dayjs.unix(node.start_at); - const endDate = node.end_at && dayjs.unix(node.end_at); + const startDate = node.start_at ? dayjs.unix(node.start_at) : null; + const endDate = node.end_at ? dayjs.unix(node.end_at) : null; let duration = endDate && startDate && endDate.diff(startDate, "hours"); if (typeof duration === "number" && duration < 1) { duration = 1; } + const durationLabel = duration + ? formatDuration( + intervalToDuration({ + start: 0, + end: duration * 60 * 60 * 1000, + }), + { + delimiter: ", ", + format: ["years", "months", "weeks", "days", "hours"], + }, + ) + : null; // Convert max_price_per_node_hour from cents to dollars const pricePerHour = node.max_price_per_node_hour ? (node.max_price_per_node_hour / 100) @@ -285,6 +357,8 @@ function NodeVerboseDisplay({ node }: { node: SFCNodes.Node }) { // Get available actions for this node const nodeActions = getActionsForNode(node); + const lastVM = getLastVM(node); + return ( + {lastVM && ( + <> + + Active VM: + + + + + + + + + + )} + + {node.vms?.data && node.vms.data.length > 1 && ( + + + + + vm.id !== lastVM?.id + )} + /> + + )} + - 📅 Schedule: + Schedule: - + )} @@ -362,9 +494,9 @@ function NodeVerboseDisplay({ node }: { node: SFCNodes.Node }) { {node.max_price_per_node_hour && ( <> - 💰 Pricing: + Pricing: - + {node.node_type === "autoreserved" && ( <> )} - {/* VMs Section - Show if node has VMs */} - {node.vms?.data && node.vms.data.length > 0 && ( - - - 💿 - - - - - - )} - - {node.vms?.data?.[0]?.image_id && ( - <> - - 💾 Current VM Image: - - - - - - )} - {/* Actions Section - Show based on available actions */} {nodeActions.length > 0 && ( <> - 🎯 Actions: + Actions: - + {nodeActions.map((action, index) => (