diff --git a/src/lib/orders/OrderDisplay.tsx b/src/lib/orders/OrderDisplay.tsx index 30e74df..af0e722 100644 --- a/src/lib/orders/OrderDisplay.tsx +++ b/src/lib/orders/OrderDisplay.tsx @@ -1,6 +1,6 @@ -import { Box, Text } from "ink"; +import { Box, measureElement, Text, useInput } from "ink"; import dayjs from "npm:dayjs@1.11.13"; -import React from "react"; +import React, { useEffect } from "react"; import { GPUS_PER_NODE } from "../constants.ts"; import { Row } from "../Row.tsx"; import { formatDuration } from "./index.tsx"; @@ -29,6 +29,9 @@ function orderDetails(order: HydratedOrder) { }; } +const formatDateTime = (date: string) => + dayjs(date).format("MMM D h:mm a").toLowerCase(); + function Order(props: { order: HydratedOrder }) { const { pricePerGPUHour, durationFormatted } = orderDetails(props.order); @@ -74,55 +77,88 @@ function Order(props: { order: HydratedOrder }) { ); } -function OrderMinimal(props: { order: HydratedOrder }) { +function OrderMinimal(props: { + order: HydratedOrder; + activeTab: "all" | "sell" | "buy"; +}) { const { pricePerGPUHour, durationFormatted, executedPricePerGPUHour } = orderDetails(props.order); return ( - - - {props.order.side === "buy" ? "↑" : "↓"} - + {props.order.side} - + - ${pricePerGPUHour.toFixed(2)}/gpu/hr + ${pricePerGPUHour.toFixed(2)} + /gpu/hr {executedPricePerGPUHour && ( ${executedPricePerGPUHour.toFixed(2)} )} - - - {dayjs(props.order.start_at).format("MMM D h:mm a").toLowerCase()} →{" "} - {dayjs(props.order.end_at).format("MMM D h:mm a").toLowerCase()} - + + {durationFormatted} + + + {formatDateTime(props.order.start_at)} + + + + + + + {dayjs(props.order.start_at).isSame(props.order.end_at, "day") + ? dayjs(props.order.end_at).format("h:mm a").toLowerCase() + : formatDateTime(props.order.end_at)} + + + - - {durationFormatted} - - - ({props.order.status}) + + {props.order.status} - {props.order.id} + {props.order.id} ); } +const NUMBER_OF_ORDERS_TO_DISPLAY = 20; + export function OrderDisplay(props: { orders: HydratedOrder[]; expanded?: boolean; }) { + const [activeTab, setActiveTab] = React.useState<"all" | "sell" | "buy">( + "all" + ); + + useInput((input, key) => { + if (key.escape || input === "q") { + process.exit(0); + } + if (input === "a") { + setActiveTab("all"); + } + + if (input === "s") { + setActiveTab("sell"); + } + + if (input === "b") { + setActiveTab("buy"); + } + }); + if (props.orders.length === 0) { return ( @@ -141,11 +177,244 @@ export function OrderDisplay(props: { ); } - return props.orders.map(order => { - return props.expanded ? ( - - ) : ( - - ); + const orders = + activeTab === "all" + ? props.orders + : props.orders.filter(order => order.side === activeTab); + + const { sellOrdersCount, buyOrdersCount } = React.useMemo(() => { + return { + sellOrdersCount: props.orders.filter(order => order.side === "sell") + .length, + buyOrdersCount: props.orders.filter(order => order.side === "buy").length, + }; + }, [props.orders]); + + return ( + <> + + {orders.map(order => { + return props.expanded ? ( + + ) : ( + + ); + })} + + {orders.length === 0 && ( + + + There are 0 outstanding {activeTab === "all" ? "" : activeTab}{" "} + orders right now. + + + )} + + + ); +} + +const reducer = (state, action) => { + switch (action.type) { + case "SET_INNER_HEIGHT": + return { + ...state, + innerHeight: action.innerHeight, + }; + + case "SCROLL_DOWN": + return { + ...state, + scrollTop: Math.min( + state.innerHeight - state.height, + state.scrollTop + 1 + ), + }; + + case "SCROLL_DOWN_BULK": + return { + ...state, + scrollTop: Math.min( + state.innerHeight - state.height, + state.scrollTop + NUMBER_OF_ORDERS_TO_DISPLAY + ), + }; + + case "SCROLL_UP": + return { + ...state, + scrollTop: Math.max(0, state.scrollTop - 1), + }; + + case "SCROLL_UP_BULK": + return { + ...state, + scrollTop: Math.max(0, state.scrollTop - NUMBER_OF_ORDERS_TO_DISPLAY), + }; + + case "SCROLL_TO_TOP": + return { + ...state, + scrollTop: 0, + }; + + case "SCROLL_TO_BOTTOM": + return { + ...state, + scrollTop: state.innerHeight - state.height, + }; + + case "SWITCHED_TAB": { + return { + ...state, + scrollTop: 0, + }; + } + + default: + return state; + } +}; + +export function ScrollArea({ + height, + children, + orders, + activeTab, + sellOrdersCount, + buyOrdersCount, +}: { + height: number; + children: React.ReactNode; + orders: HydratedOrder[]; + activeTab: "all" | "sell" | "buy"; + sellOrdersCount: number; + buyOrdersCount: number; +}) { + const [state, dispatch] = React.useReducer(reducer, { + height, + scrollTop: 0, }); + + const innerRef = React.useRef(null); + const canScrollUp = state.scrollTop > 0 && orders.length > 0; + const numberOfOrdersAboveScrollArea = state.scrollTop; + const dateRangeAboveScrollArea = + orders.length > 0 + ? `${formatDateTime(orders[0].start_at)} → ${formatDateTime(orders[numberOfOrdersAboveScrollArea - 1]?.end_at || "0")}` + : ""; + const numberOfOrdersBelowScrollArea = + orders.length - (state.scrollTop + state.height); + const dateRangeBelowScrollArea = + orders.length > 0 + ? `${formatDateTime(orders[state.scrollTop + state.height]?.start_at || "0")} → ${formatDateTime(orders[orders.length - 1].end_at)}` + : ""; + const canScrollDown = + state.scrollTop + state.height < state.innerHeight && + numberOfOrdersBelowScrollArea >= 0; + + useEffect(() => { + if (!innerRef.current) { + return; + } + + const dimensions = measureElement(innerRef.current); + + dispatch({ + type: "SET_INNER_HEIGHT", + innerHeight: dimensions.height, + }); + }, []); + + useEffect(() => { + dispatch({ type: "SWITCHED_TAB" }); + }, [activeTab]); + + useInput((input, key) => { + if (key.downArrow || input === "j") { + dispatch({ + type: "SCROLL_DOWN", + }); + } + + if (key.upArrow || input === "k") { + dispatch({ + type: "SCROLL_UP", + }); + } + + if (input === "u") { + dispatch({ + type: "SCROLL_UP_BULK", + }); + } + + if (input === "d") { + dispatch({ + type: "SCROLL_DOWN_BULK", + }); + } + + if (input === "g") { + dispatch({ + type: "SCROLL_TO_TOP", + }); + } + + if (input === "G") { + dispatch({ + type: "SCROLL_TO_BOTTOM", + }); + } + }); + + return ( + + + + + {canScrollUp + ? `↑ ${numberOfOrdersAboveScrollArea.toLocaleString()} more (${dateRangeAboveScrollArea})` + : " "} + + + + [a]ll {sellOrdersCount + buyOrdersCount} + + + [s]ell {sellOrdersCount} + + + [b]uy {buyOrdersCount} + + + + + + + {children} + + + + + + {canScrollDown + ? `↓ ${numberOfOrdersBelowScrollArea.toLocaleString()} more (${dateRangeBelowScrollArea})` + : " "} + + + + + ); }