Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/lib/contracts/ContractDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ export function ContractDisplay(props: { contract: Contract }) {
{intervalData.map((data, index) => {
return (
<Box
key={`${index}-${data.quantity}`}
key={`${props.contract.id}-${data.start.toISOString()}`}
paddingLeft={index === 0 ? 0 : COLUMN_WIDTH}
>
{index === 0 && (
Expand Down
16 changes: 8 additions & 8 deletions src/lib/images/get.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ function ImageDisplay({
upload_status: string;
sha256_hash: string | null;
};
download: { download_url: string; expires_at: number } | null;
download: { url: string; expires_at: number } | null;
}) {
const expiresAt = download?.expires_at
? new Date(download.expires_at * 1000)
Expand All @@ -51,8 +51,8 @@ function ImageDisplay({
value={
<Box flexDirection="column" paddingRight={1}>
<Text color="cyan">Use curl or wget to download.</Text>
<Link url={download.download_url} fallback={false}>
{download.download_url}
<Link url={download.url} fallback={false}>
{download.url}
</Link>
</Box>
}
Expand Down Expand Up @@ -116,11 +116,11 @@ const get = new Command("get")
// Fetch download URL if image is completed
let download = null;
if (image.upload_status === "completed") {
const { data: downloadData } = await client.GET(
"/v2/images/{id}/download",
{ params: { path: { id } } },
);
if (downloadData) {
const { data: downloadData, response: downloadResponse } =
await client.GET("/v2/images/{id}/download", {
params: { path: { id } },
});
if (downloadResponse.ok && downloadData) {
download = downloadData;
}
}
Expand Down
148 changes: 102 additions & 46 deletions src/lib/nodes/image/show.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import console from "node:console";
import { Command } from "@commander-js/extra-typings";
import type SFCNodes from "@sfcompute/nodes-sdk-alpha";
import chalk from "chalk";
import dayjs from "dayjs";
import advanced from "dayjs/plugin/advancedFormat";
import timezone from "dayjs/plugin/timezone";
import utc from "dayjs/plugin/utc";
import { Box, render, Text } from "ink";
import Link from "ink-link";
import { apiClient } from "../../../apiClient.ts";
import { logAndQuit } from "../../../helpers/errors.ts";
import { formatDate } from "../../../helpers/format-time.ts";
import { handleNodesError, nodesClient } from "../../../nodesClient.ts";
import { Row } from "../../Row.tsx";

dayjs.extend(utc);
Expand All @@ -18,20 +18,25 @@ dayjs.extend(timezone);

export function ImageDisplay({
image,
download,
}: {
image: SFCNodes.VMs.ImageGetResponse;
image: {
name: string;
id: string;
upload_status: string;
};
download: { url: string; expires_at: number } | null;
}) {
const expiresAt = image.expires_at ? new Date(image.expires_at) : null;
const expiresAt = download?.expires_at
? new Date(download.expires_at * 1000)
: null;
const isExpired = expiresAt ? expiresAt < new Date() : false;

const statusColor = isExpired ? "red" : "green";
const statusText = isExpired ? "Expired" : "Ready";

return (
<Box flexDirection="column" padding={0} width={80}>
<Box borderStyle="single" borderColor="cyan" paddingX={1}>
<Text color="cyan" bold>
Image: {image.name} ({image.image_id})
Image: {image.name} ({image.id})
</Text>
</Box>

Expand All @@ -40,62 +45,113 @@ export function ImageDisplay({
head="Status: "
value={
<Box gap={1}>
<Text color={statusColor}>{statusText}</Text>
<Text color={formatStatusColor(image.upload_status)}>
{formatStatusText(image.upload_status)}
</Text>
</Box>
}
/>
<Row
head="URL: "
value={
<Box flexDirection="column" paddingRight={1}>
<Text color="cyan">Use curl or wget to download.</Text>
<Link url={image.download_url} fallback={false}>
{image.download_url}
</Link>
</Box>
}
/>
{expiresAt && (
<Row
head="URL Expiry: "
value={
<Box gap={1}>
<Text color={isExpired ? "red" : undefined}>
{expiresAt.toISOString()}{" "}
{chalk.blackBright(
`(${formatDate(dayjs(expiresAt).toDate())} ${dayjs(
expiresAt,
).format("z")})`,
)}
</Text>
{isExpired && <Text dimColor>(Expired)</Text>}
</Box>
}
/>
{download && (
<>
<Row
head="URL: "
value={
<Box flexDirection="column" paddingRight={1}>
<Text color="cyan">Use curl or wget to download.</Text>
<Link url={download.url} fallback={false}>
{download.url}
</Link>
</Box>
}
/>
{expiresAt && (
<Row
head="URL Expiry: "
value={
<Box gap={1}>
<Text color={isExpired ? "red" : undefined}>
{expiresAt.toISOString()}{" "}
{chalk.blackBright(
`(${formatDate(dayjs(expiresAt).toDate())} ${dayjs(
expiresAt,
).format("z")})`,
)}
</Text>
{isExpired && <Text dimColor>(Expired)</Text>}
</Box>
}
/>
)}
</>
)}
</Box>
</Box>
);
}

function formatStatusColor(status: string): string {
switch (status) {
case "started":
return "green";
case "uploading":
return "yellow";
case "completed":
return "cyan";
case "failed":
return "red";
default:
return "gray";
}
}

function formatStatusText(status: string): string {
switch (status) {
case "started":
return "Started";
case "uploading":
return "Uploading";
case "completed":
return "Completed";
case "failed":
return "Failed";
default:
return "Unknown";
}
}

const show = new Command("show")
.description("Show VM image details and download URL")
.argument("<image-id>", "ID of the image")
.option("--json", "Output JSON")
.action(async (imageId, opts) => {
try {
const client = await nodesClient();
const data = await client.vms.images.get(imageId);
const client = await apiClient();

if (opts.json) {
console.log(JSON.stringify(data, null, 2));
return;
const { data: image, response } = await client.GET("/v2/images/{id}", {
params: { path: { id: imageId } },
});
if (!response.ok || !image) {
logAndQuit(
`Failed to get image: ${response.status} ${response.statusText}`,
);
}

let download = null;
if (image.upload_status === "completed") {
const { data: downloadData, response: downloadResponse } =
await client.GET("/v2/images/{id}/download", {
params: { path: { id: imageId } },
});
if (downloadResponse.ok && downloadData) {
download = downloadData;
}
}

render(<ImageDisplay image={data} />);
} catch (err) {
handleNodesError(err);
if (opts.json) {
console.log(JSON.stringify({ ...image, download }, null, 2));
return;
}

render(<ImageDisplay image={image} download={download} />);
});

export default show;
2 changes: 1 addition & 1 deletion src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2204,7 +2204,7 @@ export interface components {
*/
vmorch_ImageDiscriminator: "image";
vmorch_ImageDownloadResponse: {
download_url: string;
url: string;
expires_at: components["schemas"]["vmorch_UnixEpoch"];
sha256_hash: string;
/** Format: u-int64 */
Expand Down
Loading