Skip to content

Commit

Permalink
feat: Enhance contract state management and CLI listing (#78)
Browse files Browse the repository at this point in the history
  • Loading branch information
JohnPhamous authored Feb 23, 2025
1 parent a082203 commit 10c3d96
Show file tree
Hide file tree
Showing 6 changed files with 87 additions and 43 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,5 @@
"peerDependencies": {
"typescript": "^5.6.2"
},
"version": "0.1.49"
"version": "0.1.50"
}
5 changes: 4 additions & 1 deletion src/lib/buy/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,10 @@ function BuyOrder(props: BuyOrderProps) {
{order && order.status === "cancelled" && (
<Box gap={1} flexDirection="column">
<Text color="red">Order could not be filled: {order.id}</Text>
<Text>You were not charged. Try placing a new order with a different price, duration, or number of GPUs.</Text>
<Text>
You were not charged. Try placing a new order with a different
price, duration, or number of GPUs.
</Text>
</Box>
)}
{order && order.status !== "cancelled" && (
Expand Down
40 changes: 15 additions & 25 deletions src/lib/contracts/ContractDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: <explanation>
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 {
/**
Expand All @@ -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: "→" }),
Expand All @@ -46,7 +47,10 @@ export function createIntervalData(
instanceType,
start,
end,
state,
state: getContractState({
intervals: [interval, shape.intervals[index + 1]],
quantities: [],
}),
};
});
}
Expand Down Expand Up @@ -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<typeof Badge>["color"] | undefined;
let statusIcon: React.ReactNode;
if (startsAt > now) {
statusIcon = <Badge color="green">Upcoming</Badge>;
color = "green";
} else if (endsAt < now) {
color = "gray";
statusIcon = <Badge color="gray">Expired</Badge>;
} else {
color = "cyan";
statusIcon = <Badge color="cyan">Active</Badge>;
}
const state = getContractState(props.contract.shape);
const color = getContractStateColor(state);
const statusIcon = <Badge color={color}>{state}</Badge>;

const intervalData = createIntervalData(
props.contract.shape,
Expand Down
32 changes: 20 additions & 12 deletions src/lib/contracts/index.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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
Expand All @@ -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(<ContractList contracts={data} />);
}
// process.exit(0);
}),
);
}

async function listContracts(): Promise<Contract[]> {
async function listContracts(showAll = false): Promise<Contract[]> {
const loggedIn = await isLoggedIn();
if (!loggedIn) {
return logLoginMessageAndQuit();
Expand All @@ -63,14 +66,19 @@ async function listContracts(): Promise<Contract[]> {
);
}

// 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 ?? [],
});
}
}
Expand Down
19 changes: 15 additions & 4 deletions src/lib/contracts/types.ts
Original file line number Diff line number Diff line change
@@ -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;
32 changes: 32 additions & 0 deletions src/lib/contracts/utils.ts
Original file line number Diff line number Diff line change
@@ -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";
}
}

0 comments on commit 10c3d96

Please sign in to comment.