Frank 3 месяцев назад
Родитель
Сommit
0e12dd62a3

+ 94 - 71
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;
     }
   }
 }

+ 48 - 70
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>

+ 3 - 2
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),
     )
   }