From 10c3d965162c69358b86ac361dbd15d455611043 Mon Sep 17 00:00:00 2001 From: John Pham Date: Sun, 23 Feb 2025 11:19:20 -0800 Subject: [PATCH] feat: Enhance contract state management and CLI listing (#78) --- package.json | 2 +- src/lib/buy/index.tsx | 5 +++- src/lib/contracts/ContractDisplay.tsx | 40 ++++++++++----------------- src/lib/contracts/index.tsx | 32 +++++++++++++-------- src/lib/contracts/types.ts | 19 ++++++++++--- src/lib/contracts/utils.ts | 32 +++++++++++++++++++++ 6 files changed, 87 insertions(+), 43 deletions(-) create mode 100644 src/lib/contracts/utils.ts diff --git a/package.json b/package.json index b360a89..139cfb7 100644 --- a/package.json +++ b/package.json @@ -48,5 +48,5 @@ "peerDependencies": { "typescript": "^5.6.2" }, - "version": "0.1.49" + "version": "0.1.50" } \ No newline at end of file diff --git a/src/lib/buy/index.tsx b/src/lib/buy/index.tsx index fcfbbd9..5ec22f1 100644 --- a/src/lib/buy/index.tsx +++ b/src/lib/buy/index.tsx @@ -488,7 +488,10 @@ function BuyOrder(props: BuyOrderProps) { {order && order.status === "cancelled" && ( Order could not be filled: {order.id} - You were not charged. Try placing a new order with a different price, duration, or number of GPUs. + + You were not charged. Try placing a new order with a different + price, duration, or number of GPUs. + )} {order && order.status !== "cancelled" && ( diff --git a/src/lib/contracts/ContractDisplay.tsx b/src/lib/contracts/ContractDisplay.tsx index a76b26d..005d3cf 100644 --- a/src/lib/contracts/ContractDisplay.tsx +++ b/src/lib/contracts/ContractDisplay.tsx @@ -2,11 +2,15 @@ import { Badge } from "@inkjs/ui"; import { Box, Text } from "ink"; import { formatDateRange } from "little-date"; import ms from "ms"; -// biome-ignore lint/style/useImportType: import * as React from "react"; import { Row } from "../Row.tsx"; import { GPUS_PER_NODE } from "../constants.ts"; -import type { Contract } from "./types.ts"; +import type { ActiveContract, Contract } from "./types.ts"; +import { + type ContractState, + getContractState, + getContractStateColor, +} from "./utils.ts"; interface IntervalData { /** @@ -24,20 +28,17 @@ interface IntervalData { instanceType: string; start: Date; end: Date; - state: "Upcoming" | "Active" | "Expired"; + state: ContractState; } export function createIntervalData( - shape: Contract["shape"], + shape: ActiveContract["shape"], instanceType: string, ): IntervalData[] { - const now = new Date(); - return shape.intervals.slice(0, -1).map((interval, index) => { const start = new Date(interval); const end = new Date(shape.intervals[index + 1]); const duration = end.getTime() - start.getTime(); - const state = start > now ? "Upcoming" : end < now ? "Expired" : "Active"; return { dateRangeLabel: formatDateRange(start, end, { separator: "→" }), @@ -46,7 +47,10 @@ export function createIntervalData( instanceType, start, end, - state, + state: getContractState({ + intervals: [interval, shape.intervals[index + 1]], + quantities: [], + }), }; }); } @@ -76,23 +80,9 @@ export function ContractDisplay(props: { contract: Contract }) { return null; } - const startsAt = new Date(props.contract.shape.intervals[0]); - const endsAt = new Date( - props.contract.shape.intervals[props.contract.shape.intervals.length - 1], - ); - const now = new Date(); - let color: React.ComponentProps["color"] | undefined; - let statusIcon: React.ReactNode; - if (startsAt > now) { - statusIcon = Upcoming; - color = "green"; - } else if (endsAt < now) { - color = "gray"; - statusIcon = Expired; - } else { - color = "cyan"; - statusIcon = Active; - } + const state = getContractState(props.contract.shape); + const color = getContractStateColor(state); + const statusIcon = {state}; const intervalData = createIntervalData( props.contract.shape, diff --git a/src/lib/contracts/index.tsx b/src/lib/contracts/index.tsx index a67c7d8..229b7a6 100644 --- a/src/lib/contracts/index.tsx +++ b/src/lib/contracts/index.tsx @@ -1,6 +1,6 @@ import { Command } from "@commander-js/extra-typings"; -import * as console from "node:console"; import { render } from "ink"; +import * as console from "node:console"; import React from "react"; import { apiClient } from "../../apiClient.ts"; import { isLoggedIn } from "../../helpers/config.ts"; @@ -10,7 +10,8 @@ import { logSessionTokenExpiredAndQuit, } from "../../helpers/errors.ts"; import { ContractList } from "./ContractDisplay.tsx"; -import type { Contract } from "./types.ts"; +import type { ActiveContract, Contract } from "./types.ts"; +import { getContractState } from "./utils.ts"; export function registerContracts(program: Command) { program @@ -22,21 +23,23 @@ export function registerContracts(program: Command) { new Command("list") .alias("ls") .option("--json", "Output in JSON format") + .option( + "--all", + "Show all contracts including expired ones (Active, Upcoming, Expired)", + ) .description("List all contracts") .action(async (options) => { if (options.json) { - console.log(await listContracts()); + console.log(await listContracts(options.all)); } else { - const data = await listContracts(); - + const data = await listContracts(options.all); render(); } - // process.exit(0); }), ); } -async function listContracts(): Promise { +async function listContracts(showAll = false): Promise { const loggedIn = await isLoggedIn(); if (!loggedIn) { return logLoginMessageAndQuit(); @@ -63,14 +66,19 @@ async function listContracts(): Promise { ); } - // filter out pending contracts - // we use loop instead of filter bc type const contracts: Contract[] = []; for (const contract of data.data) { - if (contract.status === "active") { + if (contract.status === "pending") { + contracts.push(contract as Contract); + continue; + } + + const activeContract = contract as ActiveContract; + const state = getContractState(activeContract.shape); + if (showAll || state === "Active" || state === "Upcoming") { contracts.push({ - ...contract, - colocate_with: contract.colocate_with ?? [], + ...activeContract, + colocate_with: activeContract.colocate_with ?? [], }); } } diff --git a/src/lib/contracts/types.ts b/src/lib/contracts/types.ts index 6ba2f1f..9f1d7e9 100644 --- a/src/lib/contracts/types.ts +++ b/src/lib/contracts/types.ts @@ -1,14 +1,25 @@ -export interface Contract { - object: string; - status: string; +export type ContractStatus = "active" | "expired" | "upcoming" | "pending"; + +export interface BaseContract { + object: "contract"; id: string; + status: ContractStatus; +} + +export interface PendingContract extends BaseContract { + status: "pending"; +} + +export interface ActiveContract extends BaseContract { + status: ContractStatus; created_at: string; instance_type: string; shape: { - // These are date strings intervals: string[]; quantities: number[]; }; colocate_with: string[]; cluster_id?: string; } + +export type Contract = PendingContract | ActiveContract; diff --git a/src/lib/contracts/utils.ts b/src/lib/contracts/utils.ts new file mode 100644 index 0000000..5bb8579 --- /dev/null +++ b/src/lib/contracts/utils.ts @@ -0,0 +1,32 @@ +export type ContractState = "Upcoming" | "Active" | "Expired"; + +export function getContractState(shape: { + intervals: string[]; + quantities: number[]; +}): ContractState { + const now = new Date(); + const startsAt = new Date(shape.intervals[0]); + const endsAt = new Date(shape.intervals[shape.intervals.length - 1]); + + if (startsAt > now) { + return "Upcoming"; + } + + if (endsAt < now) { + return "Expired"; + } + return "Active"; +} + +export function getContractStateColor( + state: ContractState, +): "green" | "gray" | "cyan" { + switch (state) { + case "Upcoming": + return "green"; + case "Expired": + return "gray"; + case "Active": + return "cyan"; + } +}