summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorFrank <[email protected]>2025-12-03 22:40:35 -0500
committerFrank <[email protected]>2025-12-03 22:41:31 -0500
commit4ff5783e59cae829029f4363c60ab0ec03db0e60 (patch)
tree0d505cac1a27ce776c52c0888eeca83388641dd6
parentdcfeb5298323473e18f87be3c99a01e7ab29e7a7 (diff)
downloadopencode-4ff5783e59cae829029f4363c60ab0ec03db0e60.tar.gz
opencode-4ff5783e59cae829029f4363c60ab0ec03db0e60.zip
zen: fix chart loading
-rw-r--r--packages/console/app/src/routes/workspace/[id]/graph-section.tsx147
1 files changed, 69 insertions, 78 deletions
diff --git a/packages/console/app/src/routes/workspace/[id]/graph-section.tsx b/packages/console/app/src/routes/workspace/[id]/graph-section.tsx
index 46418d618..c8340286f 100644
--- a/packages/console/app/src/routes/workspace/[id]/graph-section.tsx
+++ b/packages/console/app/src/routes/workspace/[id]/graph-section.tsx
@@ -3,7 +3,7 @@ import { UsageTable } from "@opencode-ai/console-core/schema/billing.sql.js"
import { KeyTable } from "@opencode-ai/console-core/schema/key.sql.js"
import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
import { AuthTable } from "@opencode-ai/console-core/schema/auth.sql.js"
-import { createAsync, query, useParams } from "@solidjs/router"
+import { useParams } from "@solidjs/router"
import { createEffect, createMemo, onCleanup, Show, For } from "solid-js"
import { createStore } from "solid-js/store"
import { withActor } from "~/context/auth.withActor"
@@ -94,8 +94,6 @@ async function getCosts(workspaceID: string, year: number, month: number) {
}, workspaceID)
}
-const queryCosts = query(getCosts, "costs.get")
-
const MODEL_COLORS: Record<string, string> = {
"claude-sonnet-4-5": "#D4745C",
"claude-sonnet-4": "#E8B4A4",
@@ -160,45 +158,25 @@ export function GraphSection() {
keyDropdownOpen: false,
colorScheme: "light" as "light" | "dark",
})
- const initialData = createAsync(() => queryCosts(params.id!, store.year, store.month))
-
- createEffect(() => {
- if (typeof window === "undefined") return
-
- const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
- setStore({ colorScheme: mediaQuery.matches ? "dark" : "light" })
-
- const handleColorSchemeChange = (e: MediaQueryListEvent) => {
- setStore({ colorScheme: e.matches ? "dark" : "light" })
- }
-
- mediaQuery.addEventListener("change", handleColorSchemeChange)
- onCleanup(() => mediaQuery.removeEventListener("change", handleColorSchemeChange))
- })
-
const onPreviousMonth = async () => {
const month = store.month === 0 ? 11 : store.month - 1
const year = store.month === 0 ? store.year - 1 : store.year
- const data = await getCosts(params.id!, year, month)
- setStore({ month, year, data })
+ setStore({ month, year })
}
const onNextMonth = async () => {
const month = store.month === 11 ? 0 : store.month + 1
const year = store.month === 11 ? store.year + 1 : store.year
- setStore({ month, year, data: await getCosts(params.id!, year, month) })
+ setStore({ month, year })
}
const onSelectModel = (model: string | null) => setStore({ model, modelDropdownOpen: false })
const onSelectKey = (keyID: string | null) => setStore({ key: keyID, keyDropdownOpen: false })
- const getData = createMemo(() => store.data ?? initialData())
-
const getModels = createMemo(() => {
- const data = getData()
- if (!data?.usage) return []
- return Array.from(new Set(data.usage.map((row) => row.model))).sort()
+ if (!store.data?.usage) return []
+ return Array.from(new Set(store.data.usage.map((row) => row.model))).sort()
})
const getDates = createMemo(() => {
@@ -221,9 +199,7 @@ export function GraphSection() {
const isCurrentMonth = () => store.year === now.getFullYear() && store.month === now.getMonth()
const chartConfig = createMemo((): ChartConfiguration | null => {
- if (typeof window === "undefined") return null
-
- const data = getData()
+ const data = store.data
const dates = getDates()
if (!data?.usage?.length) return null
@@ -365,15 +341,32 @@ export function GraphSection() {
}
})
+ createEffect(async () => {
+ const data = await getCosts(params.id!, store.year, store.month)
+ setStore({ data })
+ })
+
createEffect(() => {
const config = chartConfig()
if (!config || !canvasRef) return
if (chartInstance) chartInstance.destroy()
chartInstance = new Chart(canvasRef, config)
+
+ onCleanup(() => chartInstance?.destroy())
})
- onCleanup(() => chartInstance?.destroy())
+ createEffect(() => {
+ const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
+ setStore({ colorScheme: mediaQuery.matches ? "dark" : "light" })
+
+ const handleColorSchemeChange = (e: MediaQueryListEvent) => {
+ setStore({ colorScheme: e.matches ? "dark" : "light" })
+ }
+
+ mediaQuery.addEventListener("change", handleColorSchemeChange)
+ onCleanup(() => mediaQuery.removeEventListener("change", handleColorSchemeChange))
+ })
return (
<section class={styles.root}>
@@ -382,55 +375,53 @@ export function GraphSection() {
<p>Usage costs broken down by model.</p>
</div>
- <Show when={getData()}>
- <div data-slot="filter-container">
- <div data-slot="month-picker">
- <button data-slot="month-button" onClick={onPreviousMonth}>
- <IconChevronLeft />
+ <div data-slot="filter-container">
+ <div data-slot="month-picker">
+ <button data-slot="month-button" onClick={onPreviousMonth}>
+ <IconChevronLeft />
+ </button>
+ <span data-slot="month-label">{formatMonthYear()}</span>
+ <button data-slot="month-button" onClick={onNextMonth} disabled={isCurrentMonth()}>
+ <IconChevronRight />
+ </button>
+ </div>
+ <Dropdown
+ trigger={store.model === null ? "All Models" : store.model}
+ open={store.modelDropdownOpen}
+ onOpenChange={(open) => setStore({ modelDropdownOpen: open })}
+ >
+ <>
+ <button data-slot="model-item" onClick={() => onSelectModel(null)}>
+ <span>All Models</span>
</button>
- <span data-slot="month-label">{formatMonthYear()}</span>
- <button data-slot="month-button" onClick={onNextMonth} disabled={isCurrentMonth()}>
- <IconChevronRight />
+ <For each={getModels()}>
+ {(model) => (
+ <button data-slot="model-item" onClick={() => onSelectModel(model)}>
+ <span>{model}</span>
+ </button>
+ )}
+ </For>
+ </>
+ </Dropdown>
+ <Dropdown
+ trigger={getKeyName(store.key)}
+ open={store.keyDropdownOpen}
+ onOpenChange={(open) => setStore({ keyDropdownOpen: open })}
+ >
+ <>
+ <button data-slot="model-item" onClick={() => onSelectKey(null)}>
+ <span>All Keys</span>
</button>
- </div>
- <Dropdown
- trigger={store.model === null ? "All Models" : store.model}
- open={store.modelDropdownOpen}
- onOpenChange={(open) => setStore({ modelDropdownOpen: open })}
- >
- <>
- <button data-slot="model-item" onClick={() => onSelectModel(null)}>
- <span>All Models</span>
- </button>
- <For each={getModels()}>
- {(model) => (
- <button data-slot="model-item" onClick={() => onSelectModel(model)}>
- <span>{model}</span>
- </button>
- )}
- </For>
- </>
- </Dropdown>
- <Dropdown
- trigger={getKeyName(store.key)}
- open={store.keyDropdownOpen}
- onOpenChange={(open) => setStore({ keyDropdownOpen: open })}
- >
- <>
- <button data-slot="model-item" onClick={() => onSelectKey(null)}>
- <span>All Keys</span>
- </button>
- <For each={getData()?.keys || []}>
- {(key) => (
- <button data-slot="model-item" onClick={() => onSelectKey(key.id)}>
- <span>{key.displayName}</span>
- </button>
- )}
- </For>
- </>
- </Dropdown>
- </div>
- </Show>
+ <For each={store.data?.keys || []}>
+ {(key) => (
+ <button data-slot="model-item" onClick={() => onSelectKey(key.id)}>
+ <span>{key.displayName}</span>
+ </button>
+ )}
+ </For>
+ </>
+ </Dropdown>
+ </div>
<Show
when={chartConfig()}