From cf67c06b7cd903fa0497547aab660c8830fa045a Mon Sep 17 00:00:00 2001 From: John Pham Date: Tue, 18 Feb 2025 15:12:35 -0800 Subject: [PATCH 01/11] refactor: Enhance ContractDisplay with improved interval rendering and UI components --- .cursor/rules/cli.mdc | 3 +- deno.lock | 59 ++++++++- package.json | 3 + src/lib/contracts/ContractDisplay.tsx | 180 ++++++++++++++++++-------- 4 files changed, 188 insertions(+), 57 deletions(-) diff --git a/.cursor/rules/cli.mdc b/.cursor/rules/cli.mdc index 93b33cb..2609372 100644 --- a/.cursor/rules/cli.mdc +++ b/.cursor/rules/cli.mdc @@ -5,4 +5,5 @@ globs: *.(ts|tsx) - This is a CLI program written in Deno using the `commander` library and `@commander-js/extra-typings`. - When using node globals like `console.log`, `setTimeout`, `process`, etc, import them from the corresponding explicit `node:console`, `node:timers`, and `node:process`, etc. -- Where possible, use the `openapi-typescript`/`openapi-fetch` client instead of writing raw fetch calls. \ No newline at end of file +- Where possible, use the `openapi-typescript`/`openapi-fetch` client instead of writing raw fetch calls. +- renders the content as uppercase so tests should assert on the uppercase text. \ No newline at end of file diff --git a/deno.lock b/deno.lock index 17cee55..f78c74a 100644 --- a/deno.lock +++ b/deno.lock @@ -4,6 +4,7 @@ "jsr:@std/assert@1": "1.0.9", "jsr:@std/internal@^1.0.5": "1.0.5", "npm:@commander-js/extra-typings@^12.1.0": "12.1.0_commander@12.1.0", + "npm:@inkjs/ui@2": "2.0.0_ink@5.1.0__@types+react@18.3.18__react@18.3.1_@types+react@18.3.18_react@18.3.1", "npm:@inquirer/prompts@^5.1.2": "5.5.0", "npm:@types/ms@~0.7.34": "0.7.34", "npm:@types/react@^18.3.12": "18.3.18", @@ -18,9 +19,11 @@ "npm:dotenv@^16.4.5": "16.4.7", "npm:ink-confirm-input@2": "2.0.0_ink@5.1.0__@types+react@18.3.18__react@18.3.1_react@18.3.1_@types+react@18.3.18", "npm:ink-spinner@5": "5.0.0_ink@5.1.0__@types+react@18.3.18__react@18.3.1_react@18.3.1_@types+react@18.3.18", + "npm:ink-testing-library@4": "4.0.0_@types+react@18.3.18", "npm:ink-text-input@6": "6.0.0_ink@5.1.0__@types+react@18.3.18__react@18.3.1_react@18.3.1_@types+react@18.3.18", "npm:ink@^5.0.1": "5.1.0_@types+react@18.3.18_react@18.3.1", "npm:inquirer@^10.1.2": "10.2.2", + "npm:little-date@1": "1.0.0", "npm:ms@^2.1.3": "2.1.3", "npm:node-fetch@^3.3.2": "3.3.2", "npm:openapi-fetch@~0.11.1": "0.11.3", @@ -54,6 +57,12 @@ "is-fullwidth-code-point@4.0.0" ] }, + "@babel/runtime@7.26.9": { + "integrity": "sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg==", + "dependencies": [ + "regenerator-runtime" + ] + }, "@colors/colors@1.5.0": { "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==" }, @@ -63,6 +72,16 @@ "commander" ] }, + "@inkjs/ui@2.0.0_ink@5.1.0__@types+react@18.3.18__react@18.3.1_@types+react@18.3.18_react@18.3.1": { + "integrity": "sha512-5+8fJmwtF9UvikzLfph9sA+LS+l37Ij/szQltkuXLOAXwNkBX9innfzh4pLGXIB59vKEQUtc6D4qGvhD7h3pAg==", + "dependencies": [ + "chalk@5.4.1", + "cli-spinners@3.2.0", + "deepmerge", + "figures", + "ink@5.1.0_@types+react@18.3.18_react@18.3.1" + ] + }, "@inquirer/checkbox@2.5.0": { "integrity": "sha512-sMgdETOfi2dUHT8r7TT1BTKOwNvdDGFDXYWtQ2J69SvlYNntk9I/gJe7r5yvMwwsuKnYbuRs3pNhx4tgNck5aA==", "dependencies": [ @@ -371,6 +390,9 @@ "cli-spinners@2.9.2": { "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==" }, + "cli-spinners@3.2.0": { + "integrity": "sha512-pXftdQloMZzjCr3pCTIRniDcys6dDzgpgVhAHHk6TKBDbRuP1MkuetTF5KSv4YUutbOPa7+7ZrAJ2kVtbMqyXA==" + }, "cli-table3@0.6.5": { "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", "dependencies": [ @@ -437,9 +459,18 @@ "data-uri-to-buffer@4.0.1": { "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==" }, + "date-fns@2.30.0": { + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dependencies": [ + "@babel/runtime" + ] + }, "dayjs@1.11.13": { "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==" }, + "deepmerge@4.3.1": { + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==" + }, "delayed-stream@1.0.0": { "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" }, @@ -479,6 +510,12 @@ "web-streams-polyfill" ] }, + "figures@6.1.0": { + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "dependencies": [ + "is-unicode-supported@2.1.0" + ] + }, "follow-redirects@1.15.9": { "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==" }, @@ -524,11 +561,17 @@ "ink-spinner@5.0.0_ink@5.1.0__@types+react@18.3.18__react@18.3.1_react@18.3.1_@types+react@18.3.18": { "integrity": "sha512-EYEasbEjkqLGyPOUc8hBJZNuC5GvXGMLu0w5gdTNskPc7Izc5vO3tdQEYnzvshucyGCBXc86ig0ujXPMWaQCdA==", "dependencies": [ - "cli-spinners", + "cli-spinners@2.9.2", "ink@5.1.0_@types+react@18.3.18_react@18.3.1", "react@18.3.1" ] }, + "ink-testing-library@4.0.0_@types+react@18.3.18": { + "integrity": "sha512-yF92kj3pmBvk7oKbSq5vEALO//o7Z9Ck/OaLNlkzXNeYdwfpxMQkSowGTFUCS5MSu9bWfSZMewGpp7bFc66D7Q==", + "dependencies": [ + "@types/react" + ] + }, "ink-text-input@3.3.0_ink@2.7.1__@types+react@18.3.18__react@16.14.0_react@16.14.0_@types+react@18.3.18": { "integrity": "sha512-gO4wrOf2ie3YuEARTIwGlw37lMjFn3Gk6CKIDrMlHb46WFMagZU7DplohjM24zynlqfnXA5UDEIfC2NBcvD8kg==", "dependencies": [ @@ -652,6 +695,12 @@ "js-tokens@4.0.0": { "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, + "little-date@1.0.0": { + "integrity": "sha512-41T/ktcwPzxC0OJ8E3wmaK0E1DL/QNR3n30kB9Dw6Ni6Eud24It8LZm70jK8lvDd+Mg+961fzKDcF6SQRL25cQ==", + "dependencies": [ + "date-fns" + ] + }, "lodash.throttle@4.1.1": { "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==" }, @@ -746,7 +795,7 @@ "dependencies": [ "chalk@5.4.1", "cli-cursor@5.0.0", - "cli-spinners", + "cli-spinners@2.9.2", "is-interactive", "is-unicode-supported@2.1.0", "log-symbols", @@ -819,6 +868,9 @@ "loose-envify" ] }, + "regenerator-runtime@0.14.1": { + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, "restore-cursor@2.0.0": { "integrity": "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==", "dependencies": [ @@ -1070,6 +1122,7 @@ "packageJson": { "dependencies": [ "npm:@commander-js/extra-typings@^12.1.0", + "npm:@inkjs/ui@2", "npm:@inquirer/prompts@^5.1.2", "npm:@types/ms@~0.7.34", "npm:@types/react@^18.3.12", @@ -1084,9 +1137,11 @@ "npm:dotenv@^16.4.5", "npm:ink-confirm-input@2", "npm:ink-spinner@5", + "npm:ink-testing-library@4", "npm:ink-text-input@6", "npm:ink@^5.0.1", "npm:inquirer@^10.1.2", + "npm:little-date@1", "npm:ms@^2.1.3", "npm:node-fetch@^3.3.2", "npm:openapi-fetch@~0.11.1", diff --git a/package.json b/package.json index d733fb1..b9ca8c5 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ }, "dependencies": { "@commander-js/extra-typings": "^12.1.0", + "@inkjs/ui": "^2.0.0", "@inquirer/prompts": "^5.1.2", "@types/ms": "^0.7.34", "axios": "^1.7.2", @@ -22,8 +23,10 @@ "ink": "^5.0.1", "ink-confirm-input": "^2.0.0", "ink-spinner": "^5.0.0", + "ink-testing-library": "^4.0.0", "ink-text-input": "^6.0.0", "inquirer": "^10.1.2", + "little-date": "^1.0.0", "ms": "^2.1.3", "node-fetch": "^3.3.2", "openapi-fetch": "^0.11.1", diff --git a/src/lib/contracts/ContractDisplay.tsx b/src/lib/contracts/ContractDisplay.tsx index dc71c09..136c89d 100644 --- a/src/lib/contracts/ContractDisplay.tsx +++ b/src/lib/contracts/ContractDisplay.tsx @@ -1,13 +1,75 @@ +import { Badge } from "@inkjs/ui"; import { Box, Text } from "ink"; -import type { Contract } from "./types.ts"; -import { Row } from "../Row.tsx"; -import dayjs from "npm:dayjs@1.11.13"; +import { formatDateRange } from "little-date"; import ms from "ms"; -import React from "react"; +// 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"; + +interface IntervalData { + /** + * e.g. "1d" or "7w" + */ + durationString: string; + /** + * e.g. "Jan 1 → Jan 2" + */ + dateRangeLabel: string; + /** + * The number of GPUs in the interval. + */ + quantity: number; + instanceType: string; + start: Date; + end: Date; + state: "Upcoming" | "Active" | "Expired"; +} + +function createIntervalData( + shape: Contract["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: "→" }), + durationString: ms(duration), + quantity: shape.quantities[index], + instanceType, + start, + end, + state, + }; + }); +} + +function IntervalDisplay({ data }: { data: IntervalData }) { + const isDimmed = data.state === "Expired"; + + return ( + + + {data.quantity * GPUS_PER_NODE} gpus + + + + {data.dateRangeLabel} + + ({data.durationString}) + [{data.state}] + + ); +} -const STARTED = "▶"; -const UPCOMING = "⏸"; +const COLUMN_WIDTH = 12; export function ContractDisplay(props: { contract: Contract }) { if (props.contract.status === "pending") { @@ -15,60 +77,67 @@ export function ContractDisplay(props: { contract: Contract }) { } const startsAt = new Date(props.contract.shape.intervals[0]); - const statusIcon = startsAt < new Date() ? STARTED : UPCOMING; + 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 = "grey"; + } else if (endsAt < now) { + color = "gray"; + statusIcon = Expired; + } else { + color = "cyan"; + statusIcon = Active; + } + + const intervalData = createIntervalData( + props.contract.shape, + props.contract.instance_type, + ); return ( {statusIcon} - {props.contract.id} + {props.contract.id} - - 0 - ? props.contract.colocate_with.join(", ") - : "-"} - /> - - - {props.contract.shape.intervals.slice(0, -1).map((interval) => { - const start = new Date(interval); - const next = new Date( - props.contract.shape.intervals[ - props.contract.shape.intervals.indexOf(interval) + 1 - ], - ); - - const duration = next.getTime() - start.getTime(); - const startString = dayjs(start).format("MMM D h:mm a").toLowerCase(); - const nextString = dayjs(next).format("MMM D h:mm a").toLowerCase(); - const durationString = ms(duration); + + + {props.contract.colocate_with.length > 0 && ( + + )} - const quantity = props.contract.shape.quantities[ - props.contract.shape.intervals.indexOf(interval) - ]; - - return ( - - - - {quantity * GPUS_PER_NODE} x {props.contract.instance_type} - {" "} - (gpus) - - - - - {startString} - - {nextString} + + {intervalData.map((data, index) => { + return ( + + {index === 0 && ( + + Orders + + )} + - ({durationString}) - - ); - })} + ); + })} + ); @@ -89,9 +158,12 @@ export function ContractList(props: { contracts: Contract[] }) { } return ( - + {props.contracts.map((contract) => ( - + ))} ); From 4f28f5ce92aefb6bee85f1bb3352072e8eddf567 Mon Sep 17 00:00:00 2001 From: John Pham Date: Tue, 18 Feb 2025 15:31:38 -0800 Subject: [PATCH 02/11] feat: Add mock contracts for testing contract display states --- src/lib/contracts/ContractDisplay.tsx | 4 +- src/lib/contracts/mock.ts | 110 ++++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 src/lib/contracts/mock.ts diff --git a/src/lib/contracts/ContractDisplay.tsx b/src/lib/contracts/ContractDisplay.tsx index 136c89d..c7b1a2e 100644 --- a/src/lib/contracts/ContractDisplay.tsx +++ b/src/lib/contracts/ContractDisplay.tsx @@ -84,8 +84,8 @@ export function ContractDisplay(props: { contract: Contract }) { let color: React.ComponentProps["color"] | undefined; let statusIcon: React.ReactNode; if (startsAt > now) { - statusIcon = Upcoming; - color = "grey"; + statusIcon = Upcoming; + color = "green"; } else if (endsAt < now) { color = "gray"; statusIcon = Expired; diff --git a/src/lib/contracts/mock.ts b/src/lib/contracts/mock.ts new file mode 100644 index 0000000..fbef5f9 --- /dev/null +++ b/src/lib/contracts/mock.ts @@ -0,0 +1,110 @@ +import type { Contract } from "./types.ts"; + +const now = new Date(); +const tomorrow = new Date(now.getTime() + 24 * 60 * 60 * 1000); +const nextWeek = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000); +const nextMonth = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000); +const twoMonths = new Date(now.getTime() + 60 * 24 * 60 * 60 * 1000); +const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000); +const lastWeek = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); + +export const MOCK_CONTRACTS: Contract[] = [ + // 1. Upcoming contract with 1 order + { + object: "contract", + status: "active", + id: "upcoming-single", + created_at: now.toISOString(), + instance_type: "a100-80gb", + shape: { + intervals: [nextWeek.toISOString(), nextMonth.toISOString()], + quantities: [2], + }, + colocate_with: [], + }, + + // 2. Active contract with many orders + { + object: "contract", + status: "active", + id: "active-multi", + created_at: lastWeek.toISOString(), + instance_type: "a100-40gb", + shape: { + intervals: [ + lastWeek.toISOString(), + yesterday.toISOString(), + tomorrow.toISOString(), + nextWeek.toISOString(), + ], + quantities: [1, 3, 2], + }, + colocate_with: ["cluster-123"], + }, + + // 3. Upcoming contract with many orders + { + object: "contract", + status: "active", + id: "upcoming-multi", + created_at: now.toISOString(), + instance_type: "a100-80gb", + shape: { + intervals: [ + nextWeek.toISOString(), + nextMonth.toISOString(), + twoMonths.toISOString(), + ], + quantities: [1, 4], + }, + colocate_with: [], + }, + + // 4. Expired contract (for testing expired state) + { + object: "contract", + status: "active", + id: "expired", + created_at: lastWeek.toISOString(), + instance_type: "a100-40gb", + shape: { + intervals: [lastWeek.toISOString(), yesterday.toISOString()], + quantities: [2], + }, + colocate_with: [], + }, + + // 5. Mixed state contract (some intervals expired, some active, some upcoming) + { + object: "contract", + status: "active", + id: "mixed-states", + created_at: lastWeek.toISOString(), + instance_type: "h100", + shape: { + intervals: [ + lastWeek.toISOString(), + yesterday.toISOString(), + tomorrow.toISOString(), + nextWeek.toISOString(), + nextMonth.toISOString(), + ], + quantities: [1, 2, 3, 4], + }, + colocate_with: ["cluster-456"], + }, + + // 6. Contract with colocate_with + { + object: "contract", + status: "active", + id: "colocated", + created_at: now.toISOString(), + instance_type: "a100-80gb", + shape: { + intervals: [nextWeek.toISOString(), nextMonth.toISOString()], + quantities: [2], + }, + colocate_with: ["cluster-789", "cluster-012"], + }, +]; From 4ef3005097310f15df75ef3082273bf9ea927374 Mon Sep 17 00:00:00 2001 From: John Pham Date: Tue, 18 Feb 2025 15:34:29 -0800 Subject: [PATCH 03/11] update schema to include contracts on clusters endpoint --- src/schema.ts | 639 +++++++++++++++++++++++++++++++++++++------------- 1 file changed, 474 insertions(+), 165 deletions(-) diff --git a/src/schema.ts b/src/schema.ts index 1657322..70322f9 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -46,13 +46,13 @@ export interface paths { get: operations["getV0GridsById"]; put?: never; post?: never; - delete?: never; + delete: operations["deleteV0GridsById"]; options?: never; head?: never; patch?: never; trace?: never; }; - "/v0/grids/{id}/grids/{id}/disable": { + "/v0/grids/{id}/disable": { parameters: { query?: never; header?: never; @@ -65,10 +65,10 @@ export interface paths { delete?: never; options?: never; head?: never; - patch: operations["patchV0GridsByIdGridsByIdDisable"]; + patch: operations["patchV0GridsByIdDisable"]; trace?: never; }; - "/v0/grids/{id}/grids/{id}/enable": { + "/v0/grids/{id}/enable": { parameters: { query?: never; header?: never; @@ -81,51 +81,83 @@ export interface paths { delete?: never; options?: never; head?: never; - patch: operations["patchV0GridsByIdGridsByIdEnable"]; + patch: operations["patchV0GridsByIdEnable"]; trace?: never; }; - "/v0/grids/{id}/grids/{id}": { + "/v0/procurements/{id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get?: never; + get: operations["getV0ProcurementsById"]; + put: operations["putV0ProcurementsById"]; + post: operations["postV0ProcurementsById"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v0/procurements": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["getV0Procurements"]; + put?: never; + post: operations["postV0Procurements"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v0/vm/logs": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["getV0VmLogs"]; put?: never; post?: never; - delete: operations["deleteV0GridsByIdGridsById"]; + delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v0/procurements/{id}": { + "/v0/vm/script": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get: operations["getV0ProcurementsById"]; - put: operations["putV0ProcurementsById"]; - post: operations["postV0ProcurementsById"]; + get?: never; + put?: never; + post: operations["postV0VmScript"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v0/procurements": { + "/v0/vm/nodes": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get: operations["getV0Procurements"]; + get: operations["getV0VmNodes"]; put?: never; - post: operations["postV0Procurements"]; + post?: never; delete?: never; options?: never; head?: never; @@ -721,7 +753,7 @@ export interface operations { }; }; }; - patchV0GridsByIdGridsByIdDisable: { + deleteV0GridsById: { parameters: { query?: never; header?: never; @@ -740,7 +772,7 @@ export interface operations { }; }; }; - patchV0GridsByIdGridsByIdEnable: { + patchV0GridsByIdDisable: { parameters: { query?: never; header?: never; @@ -759,7 +791,7 @@ export interface operations { }; }; }; - deleteV0GridsByIdGridsById: { + patchV0GridsByIdEnable: { parameters: { query?: never; header?: never; @@ -1290,6 +1322,69 @@ export interface operations { }; }; }; + getV0VmLogs: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + postV0VmScript: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + script: string; + }; + "multipart/form-data": { + script: string; + }; + "text/plain": { + script: string; + }; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getV0VmNodes: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; getV0Prices: { parameters: { query?: { @@ -1468,153 +1563,7 @@ export interface operations { headers: { [name: string]: unknown; }; - content: { - "application/json": { - data: { - /** @constant */ - object: "order"; - id: string; - side: "buy" | "sell"; - status: "pending" | "rejected" | "open" | "cancelled" | "filled" | "expired"; - /** @description The instance type. */ - instance_type: string; - /** @description The number of nodes. */ - quantity: number | string; - /** @description The start time, as an ISO 8601 string. Start times must be either "right now" or on the hour. Order start times must be in the future, and can be either the next minute from now or on the hour. For example, if it's 16:00, valid start times include 16:01, 17:00, and 18:00, but not 16:30. Dates are always rounded up to the nearest minute. */ - start_at: string; - /** @description The end time, as an ISO 8601 string. End times must be on the hour, i.e. 16:00, 17:00, 18:00, etc. 17:30, 17:01, etc are not valid end times. Dates are always rounded up to the nearest minute. */ - end_at: string; - /** @description Price in cents (1 = $0.01) */ - price: number | string; - flags: { - /** @description If true, this will be a market order. */ - market?: boolean; - /** @description If true, this is a post-only order. */ - post_only?: boolean; - /** @description If true, this is an immediate-or-cancel order. */ - ioc?: boolean; - }; - executed: boolean; - executed_at?: string; - /** @description Execution price in cents (1 = $0.01) */ - execution_price?: number | string; - cancelled: boolean; - cancelled_at?: string; - colocate_with?: string[]; - created_at: string; - }[]; - has_more: boolean; - /** @constant */ - object: "list"; - }; - "multipart/form-data": { - data: { - /** @constant */ - object: "order"; - id: string; - side: "buy" | "sell"; - status: "pending" | "rejected" | "open" | "cancelled" | "filled" | "expired"; - /** @description The instance type. */ - instance_type: string; - /** @description The number of nodes. */ - quantity: number | string; - /** @description The start time, as an ISO 8601 string. Start times must be either "right now" or on the hour. Order start times must be in the future, and can be either the next minute from now or on the hour. For example, if it's 16:00, valid start times include 16:01, 17:00, and 18:00, but not 16:30. Dates are always rounded up to the nearest minute. */ - start_at: string; - /** @description The end time, as an ISO 8601 string. End times must be on the hour, i.e. 16:00, 17:00, 18:00, etc. 17:30, 17:01, etc are not valid end times. Dates are always rounded up to the nearest minute. */ - end_at: string; - /** @description Price in cents (1 = $0.01) */ - price: number | string; - flags: { - /** @description If true, this will be a market order. */ - market?: boolean; - /** @description If true, this is a post-only order. */ - post_only?: boolean; - /** @description If true, this is an immediate-or-cancel order. */ - ioc?: boolean; - }; - executed: boolean; - executed_at?: string; - /** @description Execution price in cents (1 = $0.01) */ - execution_price?: number | string; - cancelled: boolean; - cancelled_at?: string; - colocate_with?: string[]; - created_at: string; - }[]; - has_more: boolean; - /** @constant */ - object: "list"; - }; - "text/plain": { - data: { - /** @constant */ - object: "order"; - id: string; - side: "buy" | "sell"; - status: "pending" | "rejected" | "open" | "cancelled" | "filled" | "expired"; - /** @description The instance type. */ - instance_type: string; - /** @description The number of nodes. */ - quantity: number | string; - /** @description The start time, as an ISO 8601 string. Start times must be either "right now" or on the hour. Order start times must be in the future, and can be either the next minute from now or on the hour. For example, if it's 16:00, valid start times include 16:01, 17:00, and 18:00, but not 16:30. Dates are always rounded up to the nearest minute. */ - start_at: string; - /** @description The end time, as an ISO 8601 string. End times must be on the hour, i.e. 16:00, 17:00, 18:00, etc. 17:30, 17:01, etc are not valid end times. Dates are always rounded up to the nearest minute. */ - end_at: string; - /** @description Price in cents (1 = $0.01) */ - price: number | string; - flags: { - /** @description If true, this will be a market order. */ - market?: boolean; - /** @description If true, this is a post-only order. */ - post_only?: boolean; - /** @description If true, this is an immediate-or-cancel order. */ - ioc?: boolean; - }; - executed: boolean; - executed_at?: string; - /** @description Execution price in cents (1 = $0.01) */ - execution_price?: number | string; - cancelled: boolean; - cancelled_at?: string; - colocate_with?: string[]; - created_at: string; - }[]; - has_more: boolean; - /** @constant */ - object: "list"; - }; - }; - }; - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - object: "error"; - /** @constant */ - code: "internal_server"; - message: string; - details?: Record; - }; - "multipart/form-data": { - /** @constant */ - object: "error"; - /** @constant */ - code: "internal_server"; - message: string; - details?: Record; - }; - "text/plain": { - /** @constant */ - object: "error"; - /** @constant */ - code: "internal_server"; - message: string; - details?: Record; - }; - }; + content?: never; }; }; }; @@ -2369,6 +2318,30 @@ export interface operations { name: string; kubernetes_namespace: string; kubernetes_ca_cert?: string; + contract?: { + /** @constant */ + object: "contract"; + /** @constant */ + status: "active"; + id: string; + /** Format: date-time */ + created_at: string; + /** @description The instance type. */ + instance_type: string; + /** @description A shape that describes the distribution of the contract's size over time. Must end with a quantity of 0 if not empty. */ + shape: { + intervals: string[]; + quantities: number[]; + }; + colocate_with?: string[]; + cluster_id?: string; + } | { + /** @constant */ + object: "contract"; + /** @constant */ + status: "pending"; + id: string; + }; }[]; has_more: boolean; /** @constant */ @@ -2382,6 +2355,30 @@ export interface operations { name: string; kubernetes_namespace: string; kubernetes_ca_cert?: string; + contract?: { + /** @constant */ + object: "contract"; + /** @constant */ + status: "active"; + id: string; + /** Format: date-time */ + created_at: string; + /** @description The instance type. */ + instance_type: string; + /** @description A shape that describes the distribution of the contract's size over time. Must end with a quantity of 0 if not empty. */ + shape: { + intervals: string[]; + quantities: number[]; + }; + colocate_with?: string[]; + cluster_id?: string; + } | { + /** @constant */ + object: "contract"; + /** @constant */ + status: "pending"; + id: string; + }; }[]; has_more: boolean; /** @constant */ @@ -2395,6 +2392,30 @@ export interface operations { name: string; kubernetes_namespace: string; kubernetes_ca_cert?: string; + contract?: { + /** @constant */ + object: "contract"; + /** @constant */ + status: "active"; + id: string; + /** Format: date-time */ + created_at: string; + /** @description The instance type. */ + instance_type: string; + /** @description A shape that describes the distribution of the contract's size over time. Must end with a quantity of 0 if not empty. */ + shape: { + intervals: string[]; + quantities: number[]; + }; + colocate_with?: string[]; + cluster_id?: string; + } | { + /** @constant */ + object: "contract"; + /** @constant */ + status: "pending"; + id: string; + }; }[]; has_more: boolean; /** @constant */ @@ -2504,10 +2525,58 @@ export interface operations { name: string; kubernetes_namespace: string; kubernetes_ca_cert?: string; + contract?: { + /** @constant */ + object: "contract"; + /** @constant */ + status: "active"; + id: string; + /** Format: date-time */ + created_at: string; + /** @description The instance type. */ + instance_type: string; + /** @description A shape that describes the distribution of the contract's size over time. Must end with a quantity of 0 if not empty. */ + shape: { + intervals: string[]; + quantities: number[]; + }; + colocate_with?: string[]; + cluster_id?: string; + } | { + /** @constant */ + object: "contract"; + /** @constant */ + status: "pending"; + id: string; + }; }; encrypted_token?: string; nonce?: string; ephemeral_pubkey?: string; + contracts?: ({ + /** @constant */ + object: "contract"; + /** @constant */ + status: "active"; + id: string; + /** Format: date-time */ + created_at: string; + /** @description The instance type. */ + instance_type: string; + /** @description A shape that describes the distribution of the contract's size over time. Must end with a quantity of 0 if not empty. */ + shape: { + intervals: string[]; + quantities: number[]; + }; + colocate_with?: string[]; + cluster_id?: string; + } | { + /** @constant */ + object: "contract"; + /** @constant */ + status: "pending"; + id: string; + })[]; })[]; has_more: boolean; /** @constant */ @@ -2534,10 +2603,58 @@ export interface operations { name: string; kubernetes_namespace: string; kubernetes_ca_cert?: string; + contract?: { + /** @constant */ + object: "contract"; + /** @constant */ + status: "active"; + id: string; + /** Format: date-time */ + created_at: string; + /** @description The instance type. */ + instance_type: string; + /** @description A shape that describes the distribution of the contract's size over time. Must end with a quantity of 0 if not empty. */ + shape: { + intervals: string[]; + quantities: number[]; + }; + colocate_with?: string[]; + cluster_id?: string; + } | { + /** @constant */ + object: "contract"; + /** @constant */ + status: "pending"; + id: string; + }; }; encrypted_token?: string; nonce?: string; ephemeral_pubkey?: string; + contracts?: ({ + /** @constant */ + object: "contract"; + /** @constant */ + status: "active"; + id: string; + /** Format: date-time */ + created_at: string; + /** @description The instance type. */ + instance_type: string; + /** @description A shape that describes the distribution of the contract's size over time. Must end with a quantity of 0 if not empty. */ + shape: { + intervals: string[]; + quantities: number[]; + }; + colocate_with?: string[]; + cluster_id?: string; + } | { + /** @constant */ + object: "contract"; + /** @constant */ + status: "pending"; + id: string; + })[]; })[]; has_more: boolean; /** @constant */ @@ -2564,10 +2681,58 @@ export interface operations { name: string; kubernetes_namespace: string; kubernetes_ca_cert?: string; + contract?: { + /** @constant */ + object: "contract"; + /** @constant */ + status: "active"; + id: string; + /** Format: date-time */ + created_at: string; + /** @description The instance type. */ + instance_type: string; + /** @description A shape that describes the distribution of the contract's size over time. Must end with a quantity of 0 if not empty. */ + shape: { + intervals: string[]; + quantities: number[]; + }; + colocate_with?: string[]; + cluster_id?: string; + } | { + /** @constant */ + object: "contract"; + /** @constant */ + status: "pending"; + id: string; + }; }; encrypted_token?: string; nonce?: string; ephemeral_pubkey?: string; + contracts?: ({ + /** @constant */ + object: "contract"; + /** @constant */ + status: "active"; + id: string; + /** Format: date-time */ + created_at: string; + /** @description The instance type. */ + instance_type: string; + /** @description A shape that describes the distribution of the contract's size over time. Must end with a quantity of 0 if not empty. */ + shape: { + intervals: string[]; + quantities: number[]; + }; + colocate_with?: string[]; + cluster_id?: string; + } | { + /** @constant */ + object: "contract"; + /** @constant */ + status: "pending"; + id: string; + })[]; })[]; has_more: boolean; /** @constant */ @@ -2718,10 +2883,58 @@ export interface operations { name: string; kubernetes_namespace: string; kubernetes_ca_cert?: string; + contract?: { + /** @constant */ + object: "contract"; + /** @constant */ + status: "active"; + id: string; + /** Format: date-time */ + created_at: string; + /** @description The instance type. */ + instance_type: string; + /** @description A shape that describes the distribution of the contract's size over time. Must end with a quantity of 0 if not empty. */ + shape: { + intervals: string[]; + quantities: number[]; + }; + colocate_with?: string[]; + cluster_id?: string; + } | { + /** @constant */ + object: "contract"; + /** @constant */ + status: "pending"; + id: string; + }; }; encrypted_token?: string; nonce?: string; ephemeral_pubkey?: string; + contracts?: ({ + /** @constant */ + object: "contract"; + /** @constant */ + status: "active"; + id: string; + /** Format: date-time */ + created_at: string; + /** @description The instance type. */ + instance_type: string; + /** @description A shape that describes the distribution of the contract's size over time. Must end with a quantity of 0 if not empty. */ + shape: { + intervals: string[]; + quantities: number[]; + }; + colocate_with?: string[]; + cluster_id?: string; + } | { + /** @constant */ + object: "contract"; + /** @constant */ + status: "pending"; + id: string; + })[]; }; "multipart/form-data": { /** @constant */ @@ -2743,10 +2956,58 @@ export interface operations { name: string; kubernetes_namespace: string; kubernetes_ca_cert?: string; + contract?: { + /** @constant */ + object: "contract"; + /** @constant */ + status: "active"; + id: string; + /** Format: date-time */ + created_at: string; + /** @description The instance type. */ + instance_type: string; + /** @description A shape that describes the distribution of the contract's size over time. Must end with a quantity of 0 if not empty. */ + shape: { + intervals: string[]; + quantities: number[]; + }; + colocate_with?: string[]; + cluster_id?: string; + } | { + /** @constant */ + object: "contract"; + /** @constant */ + status: "pending"; + id: string; + }; }; encrypted_token?: string; nonce?: string; ephemeral_pubkey?: string; + contracts?: ({ + /** @constant */ + object: "contract"; + /** @constant */ + status: "active"; + id: string; + /** Format: date-time */ + created_at: string; + /** @description The instance type. */ + instance_type: string; + /** @description A shape that describes the distribution of the contract's size over time. Must end with a quantity of 0 if not empty. */ + shape: { + intervals: string[]; + quantities: number[]; + }; + colocate_with?: string[]; + cluster_id?: string; + } | { + /** @constant */ + object: "contract"; + /** @constant */ + status: "pending"; + id: string; + })[]; }; "text/plain": { /** @constant */ @@ -2768,10 +3029,58 @@ export interface operations { name: string; kubernetes_namespace: string; kubernetes_ca_cert?: string; + contract?: { + /** @constant */ + object: "contract"; + /** @constant */ + status: "active"; + id: string; + /** Format: date-time */ + created_at: string; + /** @description The instance type. */ + instance_type: string; + /** @description A shape that describes the distribution of the contract's size over time. Must end with a quantity of 0 if not empty. */ + shape: { + intervals: string[]; + quantities: number[]; + }; + colocate_with?: string[]; + cluster_id?: string; + } | { + /** @constant */ + object: "contract"; + /** @constant */ + status: "pending"; + id: string; + }; }; encrypted_token?: string; nonce?: string; ephemeral_pubkey?: string; + contracts?: ({ + /** @constant */ + object: "contract"; + /** @constant */ + status: "active"; + id: string; + /** Format: date-time */ + created_at: string; + /** @description The instance type. */ + instance_type: string; + /** @description A shape that describes the distribution of the contract's size over time. Must end with a quantity of 0 if not empty. */ + shape: { + intervals: string[]; + quantities: number[]; + }; + colocate_with?: string[]; + cluster_id?: string; + } | { + /** @constant */ + object: "contract"; + /** @constant */ + status: "pending"; + id: string; + })[]; }; }; }; From a7d5611dc6adb001f8d98b58842deb7193009fed Mon Sep 17 00:00:00 2001 From: John Pham Date: Tue, 18 Feb 2025 16:11:34 -0800 Subject: [PATCH 04/11] feat: Enhance cluster listing with contract status and mock data --- src/lib/clusters/clusters.tsx | 151 +++++++++++++++++++++----- src/lib/clusters/mock.ts | 73 +++++++++++++ src/lib/clusters/types.ts | 20 ++++ src/lib/contracts/ContractDisplay.tsx | 4 +- 4 files changed, 220 insertions(+), 28 deletions(-) create mode 100644 src/lib/clusters/mock.ts create mode 100644 src/lib/clusters/types.ts diff --git a/src/lib/clusters/clusters.tsx b/src/lib/clusters/clusters.tsx index 04792c7..403e16f 100644 --- a/src/lib/clusters/clusters.tsx +++ b/src/lib/clusters/clusters.tsx @@ -1,19 +1,27 @@ import type { Command } from "@commander-js/extra-typings"; -import * as console from "node:console"; -import { clearInterval, setInterval, setTimeout } from "node:timers"; +import { Badge } from "@inkjs/ui"; import { Box, render, Text, useApp } from "ink"; import Spinner from "ink-spinner"; +import * as console from "node:console"; +import { clearInterval, setInterval, setTimeout } from "node:timers"; +// biome-ignore lint/style/useImportType: import React, { useEffect, useState } from "react"; import yaml from "yaml"; import { apiClient } from "../../apiClient.ts"; import { logAndQuit } from "../../helpers/errors.ts"; import { Row } from "../Row.tsx"; +import { + createIntervalData, + IntervalDisplay, +} from "../contracts/ContractDisplay.tsx"; import { decryptSecret, getKeys, regenerateKeys } from "./keys.tsx"; import { createKubeconfig, KUBECONFIG_PATH, syncKubeconfig, } from "./kubeconfig.ts"; +import { MOCK_CLUSTERS } from "./mock.ts"; +import type { UserFacingCluster } from "./types.ts"; export function registerClusters(program: Command) { const clusters = program @@ -96,33 +104,124 @@ export function registerClusters(program: Command) { function ClusterDisplay({ clusters, }: { - clusters: Array<{ - name: string; - kubernetes_api_url: string; - kubernetes_namespace: string; - }>; + clusters: Array; }) { return ( - + {clusters.map((cluster, index) => ( - - - - - + ))} ); } +const ClusterRow = ({ cluster }: { cluster: UserFacingCluster }) => { + if (cluster.contract) { + return ; + } + + return ( + + + + + + ); +}; + +const COLUMN_WIDTH = 11; + +const ClusterRowWithContracts = ( + { cluster }: { + cluster: UserFacingCluster; + }, +) => { + if (!cluster.contract) { + return null; + } + + const startsAt = new Date(cluster.contract.shape.intervals[0]); + const endsAt = new Date( + cluster.contract.shape + .intervals[cluster.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 intervalData = createIntervalData( + cluster.contract?.shape, + cluster.contract?.instance_type, + ); + + return ( + + + {statusIcon} + {cluster.contract.id} + + + + + + + + + + + {intervalData.map((data, index) => { + return ( + + {index === 0 && ( + + Orders + + )} + + + ); + })} + + + + ); +}; + async function listClustersAction({ returnJson, token, @@ -156,11 +255,11 @@ async function listClustersAction({ } else { render( ({ - name: cluster.name, - kubernetes_api_url: cluster.kubernetes_api_url || "", - kubernetes_namespace: cluster.kubernetes_namespace || "", - }))} + clusters={[...data.data, ...MOCK_CLUSTERS].filter(( + cluster, + ): cluster is UserFacingCluster => + cluster.contract?.status === "active" || !cluster.contract + )} />, ); } diff --git a/src/lib/clusters/mock.ts b/src/lib/clusters/mock.ts new file mode 100644 index 0000000..e1752a3 --- /dev/null +++ b/src/lib/clusters/mock.ts @@ -0,0 +1,73 @@ +import { MOCK_CONTRACTS } from "../contracts/mock.ts"; +import type { UserFacingCluster } from "./types.ts"; + +export const MOCK_CLUSTERS: UserFacingCluster[] = [ + // 1. Cluster with upcoming single order contract + { + object: "kubernetes_cluster", + kubernetes_api_url: "https://k8s-api-1.example.com", + name: "upcoming-single-cluster", + kubernetes_namespace: "default", + kubernetes_ca_cert: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t", + contract: MOCK_CONTRACTS[0] as UserFacingCluster["contract"], // upcoming-single contract + }, + + // 2. Cluster with active multi-order contract + { + object: "kubernetes_cluster", + kubernetes_api_url: "https://k8s-api-2.example.com", + name: "active-multi-cluster", + kubernetes_namespace: "prod", + kubernetes_ca_cert: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t", + contract: MOCK_CONTRACTS[1] as UserFacingCluster["contract"], // active-multi contract + }, + + // 3. Cluster with upcoming multi-order contract + { + object: "kubernetes_cluster", + kubernetes_api_url: "https://k8s-api-3.example.com", + name: "upcoming-multi-cluster", + kubernetes_namespace: "staging", + kubernetes_ca_cert: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t", + contract: MOCK_CONTRACTS[2] as UserFacingCluster["contract"], // upcoming-multi contract + }, + + // 4. Cluster with expired contract + { + object: "kubernetes_cluster", + kubernetes_api_url: "https://k8s-api-4.example.com", + name: "expired-cluster", + kubernetes_namespace: "default", + kubernetes_ca_cert: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t", + contract: MOCK_CONTRACTS[3] as UserFacingCluster["contract"], // expired contract + }, + + // 5. Cluster with mixed state contract + { + object: "kubernetes_cluster", + kubernetes_api_url: "https://k8s-api-5.example.com", + name: "mixed-states-cluster", + kubernetes_namespace: "mixed", + kubernetes_ca_cert: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t", + contract: MOCK_CONTRACTS[4] as UserFacingCluster["contract"], // mixed-states contract + }, + + // 6. Cluster with colocated contract + { + object: "kubernetes_cluster", + kubernetes_api_url: "https://k8s-api-6.example.com", + name: "colocated-cluster", + kubernetes_namespace: "colocated", + kubernetes_ca_cert: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t", + contract: MOCK_CONTRACTS[5] as UserFacingCluster["contract"], // colocated contract + }, + + // 7. Cluster without contract (for testing non-contract clusters) + { + object: "kubernetes_cluster", + kubernetes_api_url: "https://k8s-api-7.example.com", + name: "no-contract-cluster", + kubernetes_namespace: "default", + kubernetes_ca_cert: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t", + }, +]; diff --git a/src/lib/clusters/types.ts b/src/lib/clusters/types.ts new file mode 100644 index 0000000..903629a --- /dev/null +++ b/src/lib/clusters/types.ts @@ -0,0 +1,20 @@ +export interface UserFacingCluster { + object: "kubernetes_cluster"; + kubernetes_api_url?: string; + name: string; + kubernetes_namespace: string; + kubernetes_ca_cert?: string; + contract?: { + object: "contract"; + status: "active"; + id: string; + created_at: string; + instance_type: string; + shape: { + intervals: string[]; + quantities: number[]; + }; + colocate_with?: string[]; + cluster_id?: string; + }; +} diff --git a/src/lib/contracts/ContractDisplay.tsx b/src/lib/contracts/ContractDisplay.tsx index c7b1a2e..4b44225 100644 --- a/src/lib/contracts/ContractDisplay.tsx +++ b/src/lib/contracts/ContractDisplay.tsx @@ -27,7 +27,7 @@ interface IntervalData { state: "Upcoming" | "Active" | "Expired"; } -function createIntervalData( +export function createIntervalData( shape: Contract["shape"], instanceType: string, ): IntervalData[] { @@ -51,7 +51,7 @@ function createIntervalData( }); } -function IntervalDisplay({ data }: { data: IntervalData }) { +export function IntervalDisplay({ data }: { data: IntervalData }) { const isDimmed = data.state === "Expired"; return ( From 0e19e6bdbbdb9e2424543fc93e1cb2f06195caff Mon Sep 17 00:00:00 2001 From: John Pham Date: Tue, 18 Feb 2025 16:16:18 -0800 Subject: [PATCH 05/11] refactor: Improve layout and spacing in cluster and contract displays --- src/lib/clusters/clusters.tsx | 11 +++++++---- src/lib/contracts/ContractDisplay.tsx | 8 ++++++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/lib/clusters/clusters.tsx b/src/lib/clusters/clusters.tsx index 403e16f..ce7225e 100644 --- a/src/lib/clusters/clusters.tsx +++ b/src/lib/clusters/clusters.tsx @@ -20,7 +20,6 @@ import { KUBECONFIG_PATH, syncKubeconfig, } from "./kubeconfig.ts"; -import { MOCK_CLUSTERS } from "./mock.ts"; import type { UserFacingCluster } from "./types.ts"; export function registerClusters(program: Command) { @@ -175,8 +174,12 @@ const ClusterRowWithContracts = ( return ( - {statusIcon} - {cluster.contract.id} + + {statusIcon} + + + {cluster.contract.id} + @@ -255,7 +258,7 @@ async function listClustersAction({ } else { render( cluster.contract?.status === "active" || !cluster.contract diff --git a/src/lib/contracts/ContractDisplay.tsx b/src/lib/contracts/ContractDisplay.tsx index 4b44225..a76b26d 100644 --- a/src/lib/contracts/ContractDisplay.tsx +++ b/src/lib/contracts/ContractDisplay.tsx @@ -102,8 +102,12 @@ export function ContractDisplay(props: { contract: Contract }) { return ( - {statusIcon} - {props.contract.id} + + {statusIcon} + + + {props.contract.id} + Date: Tue, 18 Feb 2025 16:17:14 -0800 Subject: [PATCH 06/11] fix: Remove optional chaining for cluster contract properties --- src/lib/clusters/clusters.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/clusters/clusters.tsx b/src/lib/clusters/clusters.tsx index ce7225e..bde93cd 100644 --- a/src/lib/clusters/clusters.tsx +++ b/src/lib/clusters/clusters.tsx @@ -167,8 +167,8 @@ const ClusterRowWithContracts = ( } const intervalData = createIntervalData( - cluster.contract?.shape, - cluster.contract?.instance_type, + cluster.contract.shape, + cluster.contract.instance_type, ); return ( From ac42cf5327ede04b2be535d0f90c413b63ba1ec4 Mon Sep 17 00:00:00 2001 From: John Pham Date: Tue, 18 Feb 2025 16:19:33 -0800 Subject: [PATCH 07/11] release: v0.1.45 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index b9ca8c5..0284e66 100644 --- a/package.json +++ b/package.json @@ -48,5 +48,5 @@ "peerDependencies": { "typescript": "^5.6.2" }, - "version": "0.1.44" -} + "version": "0.1.45" +} \ No newline at end of file From f6852b6603af7dde16ed646f685f2b7797322afa Mon Sep 17 00:00:00 2001 From: John Pham Date: Tue, 18 Feb 2025 16:21:52 -0800 Subject: [PATCH 08/11] fmt --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0284e66..3f7174c 100644 --- a/package.json +++ b/package.json @@ -49,4 +49,4 @@ "typescript": "^5.6.2" }, "version": "0.1.45" -} \ No newline at end of file +} From 7e62aa7b09fc98a3c99101fef47141553b6bda77 Mon Sep 17 00:00:00 2001 From: John Pham Date: Tue, 18 Feb 2025 16:31:57 -0800 Subject: [PATCH 09/11] refactor: Simplify cluster contract display and remove interval details --- src/lib/clusters/clusters.tsx | 57 ++--------------------------------- 1 file changed, 2 insertions(+), 55 deletions(-) diff --git a/src/lib/clusters/clusters.tsx b/src/lib/clusters/clusters.tsx index bde93cd..bdba2c3 100644 --- a/src/lib/clusters/clusters.tsx +++ b/src/lib/clusters/clusters.tsx @@ -10,10 +10,6 @@ import yaml from "yaml"; import { apiClient } from "../../apiClient.ts"; import { logAndQuit } from "../../helpers/errors.ts"; import { Row } from "../Row.tsx"; -import { - createIntervalData, - IntervalDisplay, -} from "../contracts/ContractDisplay.tsx"; import { decryptSecret, getKeys, regenerateKeys } from "./keys.tsx"; import { createKubeconfig, @@ -147,39 +143,10 @@ const ClusterRowWithContracts = ( return null; } - const startsAt = new Date(cluster.contract.shape.intervals[0]); - const endsAt = new Date( - cluster.contract.shape - .intervals[cluster.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 intervalData = createIntervalData( - cluster.contract.shape, - cluster.contract.instance_type, - ); - return ( - - - {statusIcon} - - - {cluster.contract.id} - + + {cluster.contract.id} @@ -200,26 +167,6 @@ const ClusterRowWithContracts = ( head="Add User" value={`sf clusters users add --cluster ${cluster.name} --user myuser`} /> - - - {intervalData.map((data, index) => { - return ( - - {index === 0 && ( - - Orders - - )} - - - ); - })} - ); From 5b2768d50160098b0dd28308568d458f7d1b95e1 Mon Sep 17 00:00:00 2001 From: John Pham Date: Tue, 18 Feb 2025 16:32:28 -0800 Subject: [PATCH 10/11] release: v0.1.46 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 3f7174c..23b14e7 100644 --- a/package.json +++ b/package.json @@ -48,5 +48,5 @@ "peerDependencies": { "typescript": "^5.6.2" }, - "version": "0.1.45" -} + "version": "0.1.46" +} \ No newline at end of file From fe54d71134120b0d7a02708212dad08e1e8d3e26 Mon Sep 17 00:00:00 2001 From: John Pham Date: Tue, 18 Feb 2025 16:32:47 -0800 Subject: [PATCH 11/11] release: v0.1.47 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 23b14e7..6540102 100644 --- a/package.json +++ b/package.json @@ -49,4 +49,4 @@ "typescript": "^5.6.2" }, "version": "0.1.46" -} \ No newline at end of file +}