diff options
| author | Frank <[email protected]> | 2025-11-16 03:29:49 -0500 |
|---|---|---|
| committer | Frank <[email protected]> | 2025-11-16 03:29:52 -0500 |
| commit | 0e12dd62a3a4af8d00c0d1d5a445f0071bef47ab (patch) | |
| tree | 0a7aeaa74f8d01dca94450b96bba93fecff22a7b /packages | |
| parent | 2b957b5d1ce45c2413e39869a0ce9f3c81ea80fe (diff) | |
| download | opencode-0e12dd62a3a4af8d00c0d1d5a445f0071bef47ab.tar.gz opencode-0e12dd62a3a4af8d00c0d1d5a445f0071bef47ab.zip | |
zen: usage paging
Diffstat (limited to 'packages')
3 files changed, 145 insertions, 143 deletions
diff --git a/packages/console/app/src/routes/workspace/[id]/usage-section.module.css b/packages/console/app/src/routes/workspace/[id]/usage-section.module.css index 1a772ba87..2bd331bd9 100644 --- a/packages/console/app/src/routes/workspace/[id]/usage-section.module.css +++ b/packages/console/app/src/routes/workspace/[id]/usage-section.module.css @@ -1,88 +1,111 @@ -.root { - [data-component="empty-state"] { - padding: var(--space-20) var(--space-6); - text-align: center; - border: 1px dashed var(--color-border); - border-radius: var(--border-radius-sm); - display: flex; - flex-direction: column; - gap: var(--space-2); - - p { - line-height: 1.5; - font-size: var(--font-size-sm); - color: var(--color-text-muted); +/* Empty state */ +[data-component="empty-state"] { + padding: var(--space-20) var(--space-6); + text-align: center; + border: 1px dashed var(--color-border); + border-radius: var(--border-radius-sm); + + p { + font-size: var(--font-size-sm); + color: var(--color-text-muted); + } +} + +/* Table container */ +[data-slot="usage-table"] { + overflow-x: auto; +} + +/* Table element */ +[data-slot="usage-table-element"] { + width: 100%; + border-collapse: collapse; + font-size: var(--font-size-sm); + + thead { + border-bottom: 1px solid var(--color-border); + } + + th { + padding: var(--space-3) var(--space-4); + text-align: left; + font-weight: normal; + color: var(--color-text-muted); + text-transform: uppercase; + } + + td { + padding: var(--space-3) var(--space-4); + border-bottom: 1px solid var(--color-border-muted); + color: var(--color-text-muted); + font-family: var(--font-mono); + + &[data-slot="usage-date"] { + color: var(--color-text); + } + + &[data-slot="usage-model"] { + font-family: var(--font-sans); + color: var(--color-text-secondary); + max-width: 200px; + word-break: break-word; + } + + &[data-slot="usage-cost"] { + color: var(--color-text); + font-weight: 500; } } - [data-slot="usage-table"] { - overflow-x: auto; + tbody tr:last-child td { + border-bottom: none; } +} - [data-slot="usage-table-element"] { - width: 100%; - border-collapse: collapse; +/* Pagination */ +[data-slot="pagination"] { + display: flex; + justify-content: flex-end; + gap: var(--space-2); + padding: var(--space-4) 0; + border-top: 1px solid var(--color-border-muted); + margin-top: var(--space-2); + + button { + padding: var(--space-2) var(--space-4); + background: var(--color-bg-secondary); + border: 1px solid var(--color-border); + border-radius: var(--border-radius-sm); + color: var(--color-text); font-size: var(--font-size-sm); + cursor: pointer; + transition: all 0.15s ease; - thead { - border-bottom: 1px solid var(--color-border); + &:hover:not(:disabled) { + background: var(--color-bg-tertiary); + border-color: var(--color-border-hover); } - th { - padding: var(--space-3) var(--space-4); - text-align: left; - font-weight: normal; - color: var(--color-text-muted); - text-transform: uppercase; + &:disabled { + opacity: 0.5; + cursor: not-allowed; } + } +} +/* Mobile responsive */ +@media (max-width: 40rem) { + [data-slot="usage-table-element"] { + th, td { - padding: var(--space-3) var(--space-4); - border-bottom: 1px solid var(--color-border-muted); - color: var(--color-text-muted); - font-family: var(--font-mono); - - &[data-slot="usage-date"] { - color: var(--color-text); - } - - &[data-slot="usage-model"] { - font-family: var(--font-sans); - font-weight: 400; - color: var(--color-text-secondary); - max-width: 200px; - word-break: break-word; - } - - &[data-slot="usage-cost"] { - color: var(--color-text); - } - } - - tbody tr { - &:last-child td { - border-bottom: none; - } + padding: var(--space-2) var(--space-3); + font-size: var(--font-size-xs); } - @media (max-width: 40rem) { - th, - td { - padding: var(--space-2) var(--space-3); - font-size: var(--font-size-xs); - } - - th { - &:nth-child(2) /* Model */ { - display: none; - } - } - - td { - &:nth-child(2) /* Model */ { - display: none; - } - } + /* Hide Model column on mobile */ + th:nth-child(2), + td:nth-child(2) { + display: none; } } } diff --git a/packages/console/app/src/routes/workspace/[id]/usage-section.tsx b/packages/console/app/src/routes/workspace/[id]/usage-section.tsx index 3618bb7e2..df8c84133 100644 --- a/packages/console/app/src/routes/workspace/[id]/usage-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/usage-section.tsx @@ -1,91 +1,59 @@ import { Billing } from "@opencode-ai/console-core/billing.js" -import { query, useParams, createAsync } from "@solidjs/router" -import { createMemo, For, Show } from "solid-js" +import { createAsync, query, useParams } from "@solidjs/router" +import { createMemo, For, Show, createEffect } from "solid-js" import { formatDateUTC, formatDateForTable } from "../common" import { withActor } from "~/context/auth.withActor" -import styles from "./usage-section.module.css" +import "./usage-section.module.css" +import { createStore } from "solid-js/store" -const getUsageInfo = query(async (workspaceID: string) => { +const PAGE_SIZE = 50 + +async function getUsageInfo(workspaceID: string, page: number) { "use server" return withActor(async () => { - return await Billing.usages() + return await Billing.usages(page, PAGE_SIZE) }, workspaceID) -}, "usage.list") +} + +const queryUsageInfo = query(getUsageInfo, "usage.list") export function UsageSection() { const params = useParams() - // ORIGINAL CODE - COMMENTED OUT FOR TESTING - const usage = createAsync(() => getUsageInfo(params.id!)) + const usage = createAsync(() => queryUsageInfo(params.id!, 0)) + const [store, setStore] = createStore({ page: 0, usage: [] as Awaited<ReturnType<typeof getUsageInfo>> }) - // DUMMY DATA FOR TESTING - // const usage = () => [ - // { - // timeCreated: new Date(Date.now() - 86400000 * 0).toISOString(), // Today - // model: "claude-3-5-sonnet-20241022", - // inputTokens: 1247, - // outputTokens: 423, - // cost: 125400000, // $1.254 - // }, - // { - // timeCreated: new Date(Date.now() - 86400000 * 0.5).toISOString(), // 12 hours ago - // model: "claude-3-haiku-20240307", - // inputTokens: 892, - // outputTokens: 156, - // cost: 23500000, // $0.235 - // }, - // { - // timeCreated: new Date(Date.now() - 86400000 * 1).toISOString(), // Yesterday - // model: "claude-3-5-sonnet-20241022", - // inputTokens: 2134, - // outputTokens: 687, - // cost: 234700000, // $2.347 - // }, - // { - // timeCreated: new Date(Date.now() - 86400000 * 1.3).toISOString(), // 1.3 days ago - // model: "gpt-4o-mini", - // inputTokens: 567, - // outputTokens: 234, - // cost: 8900000, // $0.089 - // }, - // { - // timeCreated: new Date(Date.now() - 86400000 * 2).toISOString(), // 2 days ago - // model: "claude-3-opus-20240229", - // inputTokens: 1893, - // outputTokens: 945, - // cost: 445600000, // $4.456 - // }, - // { - // timeCreated: new Date(Date.now() - 86400000 * 2.7).toISOString(), // 2.7 days ago - // model: "gpt-4o", - // inputTokens: 1456, - // outputTokens: 532, - // cost: 156800000, // $1.568 - // }, - // { - // timeCreated: new Date(Date.now() - 86400000 * 3).toISOString(), // 3 days ago - // model: "claude-3-haiku-20240307", - // inputTokens: 634, - // outputTokens: 89, - // cost: 12300000, // $0.123 - // }, - // { - // timeCreated: new Date(Date.now() - 86400000 * 4).toISOString(), // 4 days ago - // model: "claude-3-5-sonnet-20241022", - // inputTokens: 3245, - // outputTokens: 1123, - // cost: 387200000, // $3.872 - // }, - // ] + createEffect(() => { + setStore({ usage: usage() }) + }, [usage]) + + const hasResults = createMemo(() => store.usage.length > 0) + const canGoPrev = createMemo(() => store.page > 0) + const canGoNext = createMemo(() => store.usage.length === PAGE_SIZE) + + const goPrev = async () => { + const usage = await getUsageInfo(params.id!, store.page - 1) + setStore({ + page: store.page - 1, + usage, + }) + } + const goNext = async () => { + const usage = await getUsageInfo(params.id!, store.page + 1) + setStore({ + page: store.page + 1, + usage, + }) + } return ( - <section class={styles.root}> + <section> <div data-slot="section-title"> <h2>Usage History</h2> <p>Recent API usage and costs.</p> </div> <div data-slot="usage-table"> <Show - when={usage() && usage()!.length > 0} + when={hasResults()} fallback={ <div data-component="empty-state"> <p>Make your first API call to get started.</p> @@ -103,7 +71,7 @@ export function UsageSection() { </tr> </thead> <tbody> - <For each={usage()!}> + <For each={store.usage}> {(usage) => { const date = createMemo(() => new Date(usage.timeCreated)) return ( @@ -121,6 +89,16 @@ export function UsageSection() { </For> </tbody> </table> + <Show when={canGoPrev() || canGoNext()}> + <div data-slot="pagination"> + <button disabled={!canGoPrev()} onClick={goPrev}> + ← + </button> + <button disabled={!canGoNext()} onClick={goNext}> + → + </button> + </div> + </Show> </Show> </div> </section> diff --git a/packages/console/core/src/billing.ts b/packages/console/core/src/billing.ts index 348718146..049ee29bb 100644 --- a/packages/console/core/src/billing.ts +++ b/packages/console/core/src/billing.ts @@ -57,14 +57,15 @@ export namespace Billing { ) } - export const usages = async () => { + export const usages = async (page = 0, pageSize = 50) => { return await Database.use((tx) => tx .select() .from(UsageTable) .where(eq(UsageTable.workspaceID, Actor.workspace())) .orderBy(sql`${UsageTable.timeCreated} DESC`) - .limit(100), + .limit(pageSize) + .offset(page * pageSize), ) } |
