summaryrefslogtreecommitdiffhomepage
path: root/packages/cloud/app/src/component
diff options
context:
space:
mode:
authorFrank <[email protected]>2025-09-18 10:59:01 -0400
committerFrank <[email protected]>2025-09-18 10:59:01 -0400
commit4ceabdffa07b1af8d99eb73622a4d549d99ec6d2 (patch)
tree72e2ae62084a9e24cc76caffbd1f30dafc69ea56 /packages/cloud/app/src/component
parentc87480cf931a6f8f8b55552558ef521f1918b578 (diff)
downloadopencode-4ceabdffa07b1af8d99eb73622a4d549d99ec6d2.tar.gz
opencode-4ceabdffa07b1af8d99eb73622a4d549d99ec6d2.zip
wip: zen
Diffstat (limited to 'packages/cloud/app/src/component')
-rw-r--r--packages/cloud/app/src/component/icon.tsx82
-rw-r--r--packages/cloud/app/src/component/workspace/billing-section.module.css114
-rw-r--r--packages/cloud/app/src/component/workspace/billing-section.tsx193
-rw-r--r--packages/cloud/app/src/component/workspace/common.tsx25
-rw-r--r--packages/cloud/app/src/component/workspace/key-section.module.css172
-rw-r--r--packages/cloud/app/src/component/workspace/key-section.tsx182
-rw-r--r--packages/cloud/app/src/component/workspace/monthly-limit-section.module.css102
-rw-r--r--packages/cloud/app/src/component/workspace/monthly-limit-section.tsx139
-rw-r--r--packages/cloud/app/src/component/workspace/new-user-section.module.css163
-rw-r--r--packages/cloud/app/src/component/workspace/new-user-section.tsx97
-rw-r--r--packages/cloud/app/src/component/workspace/payment-section.module.css72
-rw-r--r--packages/cloud/app/src/component/workspace/payment-section.tsx113
-rw-r--r--packages/cloud/app/src/component/workspace/usage-section.module.css88
-rw-r--r--packages/cloud/app/src/component/workspace/usage-section.tsx128
14 files changed, 0 insertions, 1670 deletions
diff --git a/packages/cloud/app/src/component/icon.tsx b/packages/cloud/app/src/component/icon.tsx
deleted file mode 100644
index a82572e62..000000000
--- a/packages/cloud/app/src/component/icon.tsx
+++ /dev/null
@@ -1,82 +0,0 @@
-import { JSX } from "solid-js"
-
-export function IconLogo(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
- return (
- <svg {...props} viewBox="0 0 289 50" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M264.5 0H288.5V8.5H272.5V16.5H288.5V25H272.5V33H288.5V41.5H264.5V0Z" fill="currentColor" />
- <path d="M248.5 0H224.5V41.5H248.5V33H232.5V8.5H248.5V0Z" fill="currentColor" />
- <path d="M256.5 8.5H248.5V33H256.5V8.5Z" fill="currentColor" />
- <path
- fill-rule="evenodd"
- clip-rule="evenodd"
- d="M184.5 0H216.5V41.5H184.5V0ZM208.5 8.5H192.5V33H208.5V8.5Z"
- fill="currentColor"
- />
- <path d="M144.5 8.5H136.5V41.5H144.5V8.5Z" fill="currentColor" />
- <path d="M136.5 0H112.5V41.5H120.5V8.5H136.5V0Z" fill="currentColor" />
- <path d="M80.5 0H104.5V8.5H88.5V16.5H104.5V25H88.5V33H104.5V41.5H80.5V0Z" fill="currentColor" />
- <path
- fill-rule="evenodd"
- clip-rule="evenodd"
- d="M40.5 0H72.5V41.5H48.5V49.5H40.5V0ZM64.5 8.5H48.5V33H64.5V8.5Z"
- fill="currentColor"
- />
- <path
- fill-rule="evenodd"
- clip-rule="evenodd"
- d="M0.5 0H32.5V41.5955H0.5V0ZM24.5 8.5H8.5V33H24.5V8.5Z"
- fill="currentColor"
- />
- <path d="M152.5 0H176.5V8.5H160.5V33H176.5V41.5H152.5V0Z" fill="currentColor" />
- </svg>
- )
-}
-
-export function IconCopy(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
- return (
- <svg {...props} viewBox="0 0 512 512">
- <rect
- width="336"
- height="336"
- x="128"
- y="128"
- fill="none"
- stroke="currentColor"
- stroke-linejoin="round"
- stroke-width="32"
- rx="57"
- ry="57"
- ></rect>
- <path
- fill="none"
- stroke="currentColor"
- stroke-linecap="round"
- stroke-linejoin="round"
- stroke-width="32"
- d="m383.5 128l.5-24a56.16 56.16 0 0 0-56-56H112a64.19 64.19 0 0 0-64 64v216a56.16 56.16 0 0 0 56 56h24"
- ></path>
- </svg>
- )
-}
-
-export function IconCheck(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
- return (
- <svg {...props} viewBox="0 0 24 24">
- <path
- fill="currentColor"
- d="M9 16.17L5.53 12.7a.996.996 0 1 0-1.41 1.41l4.18 4.18c.39.39 1.02.39 1.41 0L20.29 7.71a.996.996 0 1 0-1.41-1.41z"
- ></path>
- </svg>
- )
-}
-
-export function IconCreditCard(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
- return (
- <svg {...props} viewBox="0 0 24 24">
- <path
- fill="currentColor"
- d="M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 14H4V8h16v10z"
- />
- </svg>
- )
-}
diff --git a/packages/cloud/app/src/component/workspace/billing-section.module.css b/packages/cloud/app/src/component/workspace/billing-section.module.css
deleted file mode 100644
index 0bb5709cb..000000000
--- a/packages/cloud/app/src/component/workspace/billing-section.module.css
+++ /dev/null
@@ -1,114 +0,0 @@
-.root {
- [data-slot="section-content"] {
- display: flex;
- flex-direction: column;
- gap: var(--space-3);
- }
-
- [data-slot="reload-error"] {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: var(--space-4);
- padding: var(--space-4);
- border: 1px solid var(--color-border);
- border-radius: var(--border-radius-sm);
-
- p {
- color: var(--color-danger);
- font-size: var(--font-size-sm);
- line-height: 1.4;
- margin: 0;
- flex: 1;
- }
-
- [data-slot="create-form"] {
- display: flex;
- gap: var(--space-2);
- margin: 0;
- flex-shrink: 0;
- }
- }
- [data-slot="payment"] {
- display: flex;
- flex-direction: column;
- gap: var(--space-3);
- padding: var(--space-4);
- border: 1px solid var(--color-border);
- border-radius: var(--border-radius-sm);
- min-width: 14.5rem;
- width: fit-content;
-
- @media (max-width: 30rem) {
- width: 100%;
- }
-
- [data-slot="credit-card"] {
- padding: var(--space-3-5) var(--space-4);
- background-color: var(--color-bg-surface);
- border-radius: var(--border-radius-sm);
- display: flex;
- align-items: center;
- justify-content: space-between;
-
- [data-slot="card-icon"] {
- display: flex;
- align-items: center;
- color: var(--color-text-muted);
- }
-
- [data-slot="card-details"] {
- display: flex;
- align-items: baseline;
- gap: var(--space-1);
-
- [data-slot="secret"] {
- position: relative;
- bottom: 2px;
- font-size: var(--font-size-lg);
- color: var(--color-text-muted);
- font-weight: 400;
- }
-
- [data-slot="number"] {
- font-size: var(--font-size-3xl);
- font-weight: 500;
- color: var(--color-text);
- }
- }
- }
-
- [data-slot="button-row"] {
- display: flex;
- gap: var(--space-2);
- align-items: center;
-
- @media (max-width: 30rem) {
- flex-direction: column;
-
- > button {
- width: 100%;
- }
- }
-
- [data-slot="create-form"] {
- margin: 0;
- }
-
- /* Make Enable Billing button full width when it's the only button */
- > button {
- flex: 1;
- }
- }
- }
- [data-slot="usage"] {
- p {
- font-size: var(--font-size-sm);
- line-height: 1.5;
- color: var(--color-text-secondary);
- b {
- font-weight: 600;
- }
- }
- }
-}
diff --git a/packages/cloud/app/src/component/workspace/billing-section.tsx b/packages/cloud/app/src/component/workspace/billing-section.tsx
deleted file mode 100644
index ec314d9ef..000000000
--- a/packages/cloud/app/src/component/workspace/billing-section.tsx
+++ /dev/null
@@ -1,193 +0,0 @@
-import { json, query, action, useParams, useAction, createAsync, useSubmission } from "@solidjs/router"
-import { createMemo, Show } from "solid-js"
-import { Billing } from "@opencode/cloud-core/billing.js"
-import { withActor } from "~/context/auth.withActor"
-import { IconCreditCard } from "~/component/icon"
-import styles from "./billing-section.module.css"
-
-const createCheckoutUrl = action(async (workspaceID: string, successUrl: string, cancelUrl: string) => {
- "use server"
- return withActor(() => Billing.generateCheckoutUrl({ successUrl, cancelUrl }), workspaceID)
-}, "checkoutUrl")
-
-const reload = action(async (form: FormData) => {
- "use server"
- const workspaceID = form.get("workspaceID")?.toString()
- if (!workspaceID) return { error: "Workspace ID is required" }
- return json(await withActor(() => Billing.reload(), workspaceID), { revalidate: getBillingInfo.key })
-}, "billing.reload")
-
-const disableReload = action(async (form: FormData) => {
- "use server"
- const workspaceID = form.get("workspaceID")?.toString()
- if (!workspaceID) return { error: "Workspace ID is required" }
- return json(await withActor(() => Billing.disableReload(), workspaceID), { revalidate: getBillingInfo.key })
-}, "billing.disableReload")
-
-const createSessionUrl = action(async (workspaceID: string, returnUrl: string) => {
- "use server"
- return withActor(() => Billing.generateSessionUrl({ returnUrl }), workspaceID)
-}, "sessionUrl")
-
-const getBillingInfo = query(async (workspaceID: string) => {
- "use server"
- return withActor(async () => {
- return await Billing.get()
- }, workspaceID)
-}, "billing.get")
-
-export function BillingSection() {
- const params = useParams()
- // ORIGINAL CODE - COMMENTED OUT FOR TESTING
- const balanceInfo = createAsync(() => getBillingInfo(params.id))
- const createCheckoutUrlAction = useAction(createCheckoutUrl)
- const createCheckoutUrlSubmission = useSubmission(createCheckoutUrl)
- const createSessionUrlAction = useAction(createSessionUrl)
- const createSessionUrlSubmission = useSubmission(createSessionUrl)
- const disableReloadSubmission = useSubmission(disableReload)
- const reloadSubmission = useSubmission(reload)
-
- // DUMMY DATA FOR TESTING - UNCOMMENT ONE OF THE SCENARIOS BELOW
-
- // Scenario 1: User has not added billing details and has no balance
- // const balanceInfo = () => ({
- // balance: 0,
- // paymentMethodLast4: null as string | null,
- // reload: false,
- // reloadError: null as string | null,
- // timeReloadError: null as Date | null,
- // })
-
- // Scenario 2: User has not added billing details but has a balance
- // const balanceInfo = () => ({
- // balance: 1500000000, // $15.00
- // paymentMethodLast4: null as string | null,
- // reload: false,
- // reloadError: null as string | null,
- // timeReloadError: null as Date | null
- // })
-
- // Scenario 3: User has added billing details (reload enabled)
- // const balanceInfo = () => ({
- // balance: 750000000, // $7.50
- // paymentMethodLast4: "4242",
- // reload: true,
- // reloadError: null as string | null,
- // timeReloadError: null as Date | null
- // })
-
- // Scenario 4: User has billing details but reload failed
- // const balanceInfo = () => ({
- // balance: 250000000, // $2.50
- // paymentMethodLast4: "4242",
- // reload: true,
- // reloadError: "Your card was declined." as string,
- // timeReloadError: new Date(Date.now() - 3600000) as Date // 1 hour ago
- // })
-
- const balanceAmount = createMemo(() => {
- return ((balanceInfo()?.balance ?? 0) / 100000000).toFixed(2)
- })
-
- return (
- <section class={styles.root}>
- <div data-slot="section-title">
- <h2>Billing</h2>
- <p>
- Manage payments methods. <a href="mailto:[email protected]">Contact us</a> if you have any questions.
- </p>
- </div>
- <div data-slot="section-content">
- <Show when={balanceInfo()?.reloadError}>
- <div data-slot="reload-error">
- <p>
- Reload failed at{" "}
- {balanceInfo()?.timeReloadError!.toLocaleString("en-US", {
- month: "short",
- day: "numeric",
- hour: "numeric",
- minute: "2-digit",
- second: "2-digit",
- })}
- . Reason: {balanceInfo()?.reloadError?.replace(/\.$/, "")}. Please update your payment method and try
- again.
- </p>
- <form action={reload} method="post" data-slot="create-form">
- <input type="hidden" name="workspaceID" value={params.id} />
- <button data-color="primary" type="submit" disabled={reloadSubmission.pending}>
- {reloadSubmission.pending ? "Reloading..." : "Reload"}
- </button>
- </form>
- </div>
- </Show>
- <div data-slot="payment">
- <div data-slot="credit-card">
- <div data-slot="card-icon">
- <IconCreditCard style={{ width: "32px", height: "32px" }} />
- </div>
- <div data-slot="card-details">
- <Show when={balanceInfo()?.paymentMethodLast4} fallback={<span data-slot="number">----</span>}>
- <span data-slot="secret">••••</span>
- <span data-slot="number">{balanceInfo()?.paymentMethodLast4}</span>
- </Show>
- </div>
- </div>
- <div data-slot="button-row">
- <Show
- when={balanceInfo()?.reload}
- fallback={
- <button
- data-color="primary"
- disabled={createCheckoutUrlSubmission.pending}
- onClick={async () => {
- const baseUrl = window.location.href
- const checkoutUrl = await createCheckoutUrlAction(params.id, baseUrl, baseUrl)
- if (checkoutUrl) {
- window.location.href = checkoutUrl
- }
- }}
- >
- {createCheckoutUrlSubmission.pending ? "Loading..." : "Enable Billing"}
- </button>
- }
- >
- <button
- data-color="primary"
- disabled={createSessionUrlSubmission.pending}
- onClick={async () => {
- const baseUrl = window.location.href
- const sessionUrl = await createSessionUrlAction(params.id, baseUrl)
- if (sessionUrl) {
- window.location.href = sessionUrl
- }
- }}
- >
- {createSessionUrlSubmission.pending ? "Loading..." : "Manage Payment Methods"}
- </button>
- <form action={disableReload} method="post" data-slot="create-form">
- <input type="hidden" name="workspaceID" value={params.id} />
- <button data-color="ghost" type="submit" disabled={disableReloadSubmission.pending}>
- {disableReloadSubmission.pending ? "Disabling..." : "Disable"}
- </button>
- </form>
- </Show>
- </div>
- </div>
- <div data-slot="usage">
- <Show when={!balanceInfo()?.reload && !(balanceAmount() === "0.00" || balanceAmount() === "-0.00")}>
- <p>
- You have <b data-slot="value">${balanceAmount() === "-0.00" ? "0.00" : balanceAmount()}</b> remaining in
- your account. You can continue using the API with your remaining balance.
- </p>
- </Show>
- <Show when={balanceInfo()?.reload && !balanceInfo()?.reloadError}>
- <p>
- Your current balance is <b data-slot="value">${balanceAmount() === "-0.00" ? "0.00" : balanceAmount()}</b>
- . We'll automatically reload <b>$20</b> (+$1.23 processing fee) when it reaches <b>$5</b>.
- </p>
- </Show>
- </div>
- </div>
- </section>
- )
-}
diff --git a/packages/cloud/app/src/component/workspace/common.tsx b/packages/cloud/app/src/component/workspace/common.tsx
deleted file mode 100644
index f85fd8423..000000000
--- a/packages/cloud/app/src/component/workspace/common.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-export function formatDateForTable(date: Date) {
- const options: Intl.DateTimeFormatOptions = {
- day: "numeric",
- month: "short",
- hour: "numeric",
- minute: "2-digit",
- hour12: true,
- }
- return date.toLocaleDateString("en-GB", options).replace(",", ",")
-}
-
-export function formatDateUTC(date: Date) {
- const options: Intl.DateTimeFormatOptions = {
- weekday: "short",
- year: "numeric",
- month: "short",
- day: "numeric",
- hour: "numeric",
- minute: "2-digit",
- second: "2-digit",
- timeZoneName: "short",
- timeZone: "UTC",
- }
- return date.toLocaleDateString("en-US", options)
-}
diff --git a/packages/cloud/app/src/component/workspace/key-section.module.css b/packages/cloud/app/src/component/workspace/key-section.module.css
deleted file mode 100644
index 6a1d0c85f..000000000
--- a/packages/cloud/app/src/component/workspace/key-section.module.css
+++ /dev/null
@@ -1,172 +0,0 @@
-.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);
- }
- }
-
- [data-slot="create-form"] {
- display: flex;
- flex-direction: column;
- gap: var(--space-3);
- padding: var(--space-4);
- border: 1px solid var(--color-border);
- border-radius: var(--border-radius-sm);
-
- [data-slot="input-container"] {
- display: flex;
- flex-direction: column;
- gap: var(--space-1);
- }
-
- @media (max-width: 30rem) {
- gap: var(--space-2);
- }
-
- input {
- flex: 1;
- padding: var(--space-2) var(--space-3);
- border: 1px solid var(--color-border);
- border-radius: var(--border-radius-sm);
- background-color: var(--color-bg);
- color: var(--color-text);
- font-size: var(--font-size-sm);
- font-family: var(--font-mono);
-
- &:focus {
- outline: none;
- border-color: var(--color-accent);
- }
-
- &::placeholder {
- color: var(--color-text-disabled);
- }
- }
-
- [data-slot="form-actions"] {
- display: flex;
- gap: var(--space-2);
- }
-
- [data-slot="form-error"] {
- color: var(--color-danger);
- font-size: var(--font-size-sm);
- margin-top: var(--space-1);
- line-height: 1.4;
- }
- }
-
- [data-slot="api-keys-table"] {
- overflow-x: auto;
- }
-
- [data-slot="api-keys-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="key-name"] {
- color: var(--color-text);
- font-family: var(--font-sans);
- font-weight: 500;
- }
-
- &[data-slot="key-value"] {
- font-family: var(--font-mono);
-
- button {
- display: flex;
- align-items: center;
- gap: var(--space-2);
- padding: var(--space-2) var(--space-3);
- font-size: var(--font-size-sm);
- font-weight: 400;
- border: none;
- background-color: transparent;
- color: var(--color-text-muted);
- font-family: var(--font-mono);
- border-radius: var(--border-radius-sm);
- cursor: pointer;
- transition: all 0.15s ease;
- text-transform: none;
-
- &:hover:not(:disabled) {
- background-color: var(--color-bg-surface);
- color: var(--color-text);
- }
-
- &:disabled {
- cursor: default;
- color: var(--color-text);
- }
-
- span {
- font-family: inherit;
- }
- }
- }
-
- &[data-slot="key-date"] {
- color: var(--color-text);
- }
-
- &[data-slot="key-actions"] {
- font-family: var(--font-sans);
- }
- }
-
- tbody tr {
- &:last-child td {
- border-bottom: none;
- }
- }
-
- @media (max-width: 40rem) {
- th,
- td {
- padding: var(--space-2) var(--space-3);
- font-size: var(--font-size-xs);
- }
-
- th {
- &:nth-child(3) /* Date */ {
- display: none;
- }
- }
-
- td {
- &:nth-child(3) /* Date */ {
- display: none;
- }
- }
- }
- }
-}
diff --git a/packages/cloud/app/src/component/workspace/key-section.tsx b/packages/cloud/app/src/component/workspace/key-section.tsx
deleted file mode 100644
index 4158ce793..000000000
--- a/packages/cloud/app/src/component/workspace/key-section.tsx
+++ /dev/null
@@ -1,182 +0,0 @@
-import { json, query, action, useParams, createAsync, useSubmission } from "@solidjs/router"
-import { createEffect, createSignal, For, Show } from "solid-js"
-import { IconCopy, IconCheck } from "~/component/icon"
-import { Key } from "@opencode/cloud-core/key.js"
-import { withActor } from "~/context/auth.withActor"
-import { createStore } from "solid-js/store"
-import { formatDateUTC, formatDateForTable } from "./common"
-import styles from "./key-section.module.css"
-
-const removeKey = action(async (form: FormData) => {
- "use server"
- const id = form.get("id")?.toString()
- if (!id) return { error: "ID is required" }
- const workspaceID = form.get("workspaceID")?.toString()
- if (!workspaceID) return { error: "Workspace ID is required" }
- return json(await withActor(() => Key.remove({ id }), workspaceID), { revalidate: listKeys.key })
-}, "key.remove")
-
-const createKey = action(async (form: FormData) => {
- "use server"
- const name = form.get("name")?.toString().trim()
- if (!name) return { error: "Name is required" }
- const workspaceID = form.get("workspaceID")?.toString()
- if (!workspaceID) return { error: "Workspace ID is required" }
- return json(
- await withActor(
- () =>
- Key.create({ name })
- .then((data) => ({ error: undefined, data }))
- .catch((e) => ({ error: e.message as string })),
- workspaceID,
- ),
- { revalidate: listKeys.key },
- )
-}, "key.create")
-
-const listKeys = query(async (workspaceID: string) => {
- "use server"
- return withActor(() => Key.list(), workspaceID)
-}, "key.list")
-
-export function KeyCreateForm() {
- const params = useParams()
- const submission = useSubmission(createKey)
- const [store, setStore] = createStore({ show: false })
-
- let input: HTMLInputElement
-
- createEffect(() => {
- if (!submission.pending && submission.result && !submission.result.error) {
- hide()
- }
- })
-
- function show() {
- // submission.clear() does not clear the result in some cases, ie.
- // 1. Create key with empty name => error shows
- // 2. Put in a key name and creates the key => form hides
- // 3. Click add key button again => form shows with the same error if
- // submission.clear() is called only once
- while (true) {
- submission.clear()
- if (!submission.result) break
- }
- setStore("show", true)
- input.focus()
- }
-
- function hide() {
- setStore("show", false)
- }
-
- return (
- <Show
- when={store.show}
- fallback={
- <button data-color="primary" onClick={() => show()}>
- Create API Key
- </button>
- }
- >
- <form action={createKey} method="post" data-slot="create-form">
- <div data-slot="input-container">
- <input ref={(r) => (input = r)} data-component="input" name="name" type="text" placeholder="Enter key name" />
- <Show when={submission.result && submission.result.error}>
- {(err) => <div data-slot="form-error">{err()}</div>}
- </Show>
- </div>
- <input type="hidden" name="workspaceID" value={params.id} />
- <div data-slot="form-actions">
- <button type="reset" data-color="ghost" onClick={() => hide()}>
- Cancel
- </button>
- <button type="submit" data-color="primary" disabled={submission.pending}>
- {submission.pending ? "Creating..." : "Create"}
- </button>
- </div>
- </form>
- </Show>
- )
-}
-
-export function KeySection() {
- const params = useParams()
- const keys = createAsync(() => listKeys(params.id))
-
- function formatKey(key: string) {
- if (key.length <= 11) return key
- return `${key.slice(0, 7)}...${key.slice(-4)}`
- }
-
- return (
- <section class={styles.root}>
- <div data-slot="section-title">
- <h2>API Keys</h2>
- <p>Manage your API keys for accessing opencode services.</p>
- </div>
- <KeyCreateForm />
- <div data-slot="api-keys-table">
- <Show
- when={keys()?.length}
- fallback={
- <div data-component="empty-state">
- <p>Create an opencode Gateway API key</p>
- </div>
- }
- >
- <table data-slot="api-keys-table-element">
- <thead>
- <tr>
- <th>Name</th>
- <th>Key</th>
- <th>Created</th>
- <th></th>
- </tr>
- </thead>
- <tbody>
- <For each={keys()!}>
- {(key) => {
- const [copied, setCopied] = createSignal(false)
- // const submission = useSubmission(removeKey, ([fd]) => fd.get("id")?.toString() === key.id)
- return (
- <tr>
- <td data-slot="key-name">{key.name}</td>
- <td data-slot="key-value">
- <button
- data-color="ghost"
- disabled={copied()}
- onClick={async () => {
- await navigator.clipboard.writeText(key.key)
- setCopied(true)
- setTimeout(() => setCopied(false), 1000)
- }}
- title="Copy API key"
- >
- <span>{formatKey(key.key)}</span>
- <Show when={copied()} fallback={<IconCopy style={{ width: "14px", height: "14px" }} />}>
- <IconCheck style={{ width: "14px", height: "14px" }} />
- </Show>
- </button>
- </td>
- <td data-slot="key-date" title={formatDateUTC(key.timeCreated)}>
- {formatDateForTable(key.timeCreated)}
- </td>
- <td data-slot="key-actions">
- <form action={removeKey} method="post">
- <input type="hidden" name="id" value={key.id} />
- <input type="hidden" name="workspaceID" value={params.id} />
- <button data-color="ghost">Delete</button>
- </form>
- </td>
- </tr>
- )
- }}
- </For>
- </tbody>
- </table>
- </Show>
- </div>
- </section>
- )
-}
diff --git a/packages/cloud/app/src/component/workspace/monthly-limit-section.module.css b/packages/cloud/app/src/component/workspace/monthly-limit-section.module.css
deleted file mode 100644
index 02de058e4..000000000
--- a/packages/cloud/app/src/component/workspace/monthly-limit-section.module.css
+++ /dev/null
@@ -1,102 +0,0 @@
-.root {
- [data-slot="section-content"] {
- display: flex;
- flex-direction: column;
- gap: var(--space-3);
- }
-
- [data-slot="balance"] {
- display: flex;
- flex-direction: column;
- gap: var(--space-3);
- padding: var(--space-4);
- border: 1px solid var(--color-border);
- border-radius: var(--border-radius-sm);
- min-width: 15rem;
- width: fit-content;
-
- @media (max-width: 30rem) {
- width: 100%;
- }
-
- [data-slot="amount"] {
- padding: var(--space-3-5) var(--space-4);
- background-color: var(--color-bg-surface);
- border-radius: var(--border-radius-sm);
- display: flex;
- align-items: baseline;
- gap: var(--space-1);
- justify-content: flex-end;
-
- [data-slot="currency"] {
- position: relative;
- bottom: 2px;
- font-size: var(--font-size-lg);
- color: var(--color-text-muted);
- font-weight: 400;
- }
-
- [data-slot="value"] {
- font-size: var(--font-size-3xl);
- font-weight: 500;
- color: var(--color-text);
- }
- }
-
- [data-slot="create-form"] {
- display: flex;
- flex-direction: column;
- gap: var(--space-3);
- margin-top: var(--space-1);
-
- [data-slot="input-container"] {
- display: flex;
- flex-direction: column;
- gap: var(--space-1);
- }
-
- @media (max-width: 30rem) {
- gap: var(--space-2);
- }
-
- input {
- flex: 1;
- padding: var(--space-2) var(--space-3);
- border: 1px solid var(--color-border);
- border-radius: var(--border-radius-sm);
- background-color: var(--color-bg);
- color: var(--color-text);
- font-size: var(--font-size-sm);
- font-family: var(--font-mono);
-
- &:focus {
- outline: none;
- border-color: var(--color-accent);
- }
-
- &::placeholder {
- color: var(--color-text-disabled);
- }
- }
-
- [data-slot="form-actions"] {
- display: flex;
- gap: var(--space-2);
- justify-content: flex-end;
- }
-
- [data-slot="form-error"] {
- color: var(--color-danger);
- font-size: var(--font-size-sm);
- line-height: 1.4;
- }
- }
- }
-
- [data-slot="usage-status"] {
- font-size: var(--font-size-sm);
- color: var(--color-text-secondary);
- margin: 0;
- line-height: 1.4;
- }
-}
diff --git a/packages/cloud/app/src/component/workspace/monthly-limit-section.tsx b/packages/cloud/app/src/component/workspace/monthly-limit-section.tsx
deleted file mode 100644
index 5c1077ab1..000000000
--- a/packages/cloud/app/src/component/workspace/monthly-limit-section.tsx
+++ /dev/null
@@ -1,139 +0,0 @@
-import { json, query, action, useParams, createAsync, useSubmission } from "@solidjs/router"
-import { createEffect, Show } from "solid-js"
-import { createStore } from "solid-js/store"
-import { withActor } from "~/context/auth.withActor"
-import { Billing } from "@opencode/cloud-core/billing.js"
-import styles from "./monthly-limit-section.module.css"
-
-const getBillingInfo = query(async (workspaceID: string) => {
- "use server"
- return withActor(async () => {
- return await Billing.get()
- }, workspaceID)
-}, "billing.get")
-
-const setMonthlyLimit = action(async (form: FormData) => {
- "use server"
- const limit = form.get("limit")?.toString()
- if (!limit) return { error: "Limit is required." }
- const numericLimit = parseInt(limit)
- if (numericLimit < 0) return { error: "Set a valid monthly limit." }
- const workspaceID = form.get("workspaceID")?.toString()
- if (!workspaceID) return { error: "Workspace ID is required." }
- return json(
- await withActor(
- () =>
- Billing.setMonthlyLimit(numericLimit)
- .then((data) => ({ error: undefined, data }))
- .catch((e) => ({ error: e.message as string })),
- workspaceID,
- ),
- { revalidate: getBillingInfo.key },
- )
-}, "billing.setMonthlyLimit")
-
-export function MonthlyLimitSection() {
- const params = useParams()
- const submission = useSubmission(setMonthlyLimit)
- const [store, setStore] = createStore({ show: false })
- const balanceInfo = createAsync(() => getBillingInfo(params.id))
-
- let input: HTMLInputElement
-
- createEffect(() => {
- if (!submission.pending && submission.result && !submission.result.error) {
- hide()
- }
- })
-
- function show() {
- // submission.clear() does not clear the result in some cases, ie.
- // 1. Create key with empty name => error shows
- // 2. Put in a key name and creates the key => form hides
- // 3. Click add key button again => form shows with the same error if
- // submission.clear() is called only once
- while (true) {
- submission.clear()
- if (!submission.result) break
- }
- setStore("show", true)
- input.focus()
- }
-
- function hide() {
- setStore("show", false)
- }
-
- return (
- <section class={styles.root}>
- <div data-slot="section-title">
- <h2>Monthly Limit</h2>
- <p>Set a monthly spending limit for your account.</p>
- </div>
- <div data-slot="section-content">
- <div data-slot="balance">
- <div data-slot="amount">
- {balanceInfo()?.monthlyLimit ? <span data-slot="currency">$</span> : null}
- <span data-slot="value">{balanceInfo()?.monthlyLimit ?? "-"}</span>
- </div>
- <Show
- when={!store.show}
- fallback={
- <form action={setMonthlyLimit} method="post" data-slot="create-form">
- <div data-slot="input-container">
- <input
- required
- ref={(r) => (input = r)}
- data-component="input"
- name="limit"
- type="number"
- placeholder="50"
- />
- <Show when={submission.result && submission.result.error}>
- {(err) => <div data-slot="form-error">{err()}</div>}
- </Show>
- </div>
- <input type="hidden" name="workspaceID" value={params.id} />
- <div data-slot="form-actions">
- <button type="reset" data-color="ghost" onClick={() => hide()}>
- Cancel
- </button>
- <button type="submit" data-color="primary" disabled={submission.pending}>
- {submission.pending ? "Setting..." : "Set"}
- </button>
- </div>
- </form>
- }
- >
- <button data-color="primary" onClick={() => show()}>
- {balanceInfo()?.monthlyLimit ? "Edit Limit" : "Set Limit"}
- </button>
- </Show>
- </div>
- <Show when={balanceInfo()?.monthlyLimit} fallback={<p data-slot="usage-status">No spending limit set.</p>}>
- <p data-slot="usage-status">
- Current usage for {new Date().toLocaleDateString("en-US", { month: "long", timeZone: "UTC" })} is $
- {(() => {
- const dateLastUsed = balanceInfo()?.timeMonthlyUsageUpdated
- if (!dateLastUsed) return "0"
-
- const current = new Date().toLocaleDateString("en-US", {
- year: "numeric",
- month: "long",
- timeZone: "UTC",
- })
- const lastUsed = dateLastUsed.toLocaleDateString("en-US", {
- year: "numeric",
- month: "long",
- timeZone: "UTC",
- })
- if (current !== lastUsed) return "0"
- return ((balanceInfo()?.monthlyUsage ?? 0) / 100000000).toFixed(2)
- })()}
- .
- </p>
- </Show>
- </div>
- </section>
- )
-}
diff --git a/packages/cloud/app/src/component/workspace/new-user-section.module.css b/packages/cloud/app/src/component/workspace/new-user-section.module.css
deleted file mode 100644
index 2edc7cc14..000000000
--- a/packages/cloud/app/src/component/workspace/new-user-section.module.css
+++ /dev/null
@@ -1,163 +0,0 @@
-.root {
- display: flex;
- flex-direction: column;
- gap: var(--space-8);
- padding: var(--space-6);
- background-color: var(--color-bg-surface);
- border: 1px dashed var(--color-border);
- border-radius: var(--border-radius-sm);
-
- @media (max-width: 30rem) {
- gap: var(--space-8);
- padding: var(--space-4);
- }
-
- [data-component="feature-grid"] {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
- gap: var(--space-6);
-
- @media (max-width: 30rem) {
- grid-template-columns: 1fr;
- gap: var(--space-4);
- }
-
- [data-slot="feature"] {
- display: flex;
- flex-direction: column;
- gap: var(--space-2);
- padding: var(--space-4);
- border: 1px solid var(--color-border);
- border-radius: var(--border-radius-sm);
-
- h3 {
- font-size: var(--font-size-sm);
- font-weight: 600;
- margin: 0;
- color: var(--color-text);
- text-transform: uppercase;
- letter-spacing: -0.025rem;
- }
-
- p {
- font-size: var(--font-size-sm);
- line-height: 1.5;
- margin: 0;
- color: var(--color-text-muted);
- }
- }
- }
-
- [data-component="api-key-highlight"] {
- display: flex;
- flex-direction: column;
- gap: var(--space-6);
-
- [data-slot="section-title"] {
- display: flex;
- flex-direction: column;
- gap: var(--space-1);
-
- h2 {
- font-size: var(--font-size-md);
- font-weight: 600;
- line-height: 1.2;
- letter-spacing: -0.03125rem;
- margin: 0;
- color: var(--color-text-secondary);
- text-transform: uppercase;
-
- @media (max-width: 30rem) {
- font-size: var(--font-size-md);
- }
- }
- }
-
- [data-slot="key-display"] {
- display: flex;
- flex-direction: column;
- gap: var(--space-3);
-
- [data-slot="key-container"] {
- display: flex;
- gap: var(--space-3);
- padding: var(--space-4);
- border: 2px solid var(--color-accent);
- border-radius: var(--border-radius-sm);
- align-items: center;
-
- @media (max-width: 40rem) {
- flex-direction: column;
- gap: var(--space-3);
- align-items: stretch;
- }
-
- [data-slot="key-value"] {
- flex: 1;
- font-family: var(--font-mono);
- font-size: var(--font-size-sm);
- color: var(--color-text);
- background-color: var(--color-bg);
- padding: var(--space-3);
- border-radius: var(--border-radius-sm);
- border: 1px solid var(--color-border);
- word-break: break-all;
- line-height: 1.4;
-
- @media (max-width: 40rem) {
- font-size: var(--font-size-xs);
- padding: var(--space-2-5);
- }
- }
-
- button {
- display: flex;
- align-items: center;
- gap: var(--space-2);
- padding: var(--space-3) var(--space-4);
- font-size: var(--font-size-sm);
- font-weight: 500;
- white-space: nowrap;
- min-width: 130px;
-
- @media (max-width: 40rem) {
- justify-content: center;
- padding: var(--space-2-5) var(--space-3);
- font-size: var(--font-size-xs);
- min-width: 96px;
- }
- }
- }
- }
- }
-
- [data-component="next-steps"] {
- display: flex;
- flex-direction: column;
- gap: var(--space-6);
-
- ol {
- margin: 0;
- padding-left: 0;
- display: flex;
- flex-direction: column;
- gap: var(--space-2);
- list-style-position: inside;
-
- li {
- font-size: var(--font-size-md);
- line-height: 1.5;
- color: var(--color-text-secondary);
-
- code {
- font-family: var(--font-mono);
- font-size: var(--font-size-sm);
- padding: var(--space-1) var(--space-2);
- border: 1px solid var(--color-border);
- border-radius: var(--border-radius-sm);
- color: var(--color-text);
- }
- }
- }
- }
-}
diff --git a/packages/cloud/app/src/component/workspace/new-user-section.tsx b/packages/cloud/app/src/component/workspace/new-user-section.tsx
deleted file mode 100644
index 6e031e371..000000000
--- a/packages/cloud/app/src/component/workspace/new-user-section.tsx
+++ /dev/null
@@ -1,97 +0,0 @@
-import { query, useParams, createAsync } from "@solidjs/router"
-import { createMemo, createSignal, Show } from "solid-js"
-import { IconCopy, IconCheck } from "~/component/icon"
-import { Key } from "@opencode/cloud-core/key.js"
-import { Billing } from "@opencode/cloud-core/billing.js"
-import { withActor } from "~/context/auth.withActor"
-import styles from "./new-user-section.module.css"
-
-const getUsageInfo = query(async (workspaceID: string) => {
- "use server"
- return withActor(async () => {
- return await Billing.usages()
- }, workspaceID)
-}, "usage.list")
-
-const listKeys = query(async (workspaceID: string) => {
- "use server"
- return withActor(() => Key.list(), workspaceID)
-}, "key.list")
-
-export function NewUserSection() {
- const params = useParams()
- const [copiedKey, setCopiedKey] = createSignal(false)
- const keys = createAsync(() => listKeys(params.id))
- const usage = createAsync(() => getUsageInfo(params.id))
- const isNew = createMemo(() => {
- const keysList = keys()
- const usageList = usage()
- return keysList?.length === 1 && (!usageList || usageList.length === 0)
- })
- const defaultKey = createMemo(() => keys()?.at(-1)?.key)
-
- return (
- <Show when={isNew()}>
- <div class={styles.root}>
- <div data-component="feature-grid">
- <div data-slot="feature">
- <h3>Tested & Verified Models</h3>
- <p>We've benchmarked and tested models specifically for coding agents to ensure the best performance.</p>
- </div>
- <div data-slot="feature">
- <h3>Highest Quality</h3>
- <p>Access models configured for optimal performance - no downgrades or routing to cheaper providers.</p>
- </div>
- <div data-slot="feature">
- <h3>No Lock-in</h3>
- <p>Use Zen with any coding agent, and continue using other providers with opencode whenever you want.</p>
- </div>
- </div>
-
- <div data-component="api-key-highlight">
- <Show when={defaultKey()}>
- <div data-slot="key-display">
- <div data-slot="key-container">
- <code data-slot="key-value">{defaultKey()}</code>
- <button
- data-color="primary"
- disabled={copiedKey()}
- onClick={async () => {
- await navigator.clipboard.writeText(defaultKey() ?? "")
- setCopiedKey(true)
- setTimeout(() => setCopiedKey(false), 2000)
- }}
- title="Copy API key"
- >
- <Show
- when={copiedKey()}
- fallback={
- <>
- <IconCopy style={{ width: "16px", height: "16px" }} /> Copy Key
- </>
- }
- >
- <IconCheck style={{ width: "16px", height: "16px" }} /> Copied!
- </Show>
- </button>
- </div>
- </div>
- </Show>
- </div>
-
- <div data-component="next-steps">
- <ol>
- <li>Enable billing</li>
- <li>
- Run <code>opencode auth login</code> and select opencode
- </li>
- <li>Paste your API key</li>
- <li>
- Start opencode and run <code>/models</code> to select a model
- </li>
- </ol>
- </div>
- </div>
- </Show>
- )
-}
diff --git a/packages/cloud/app/src/component/workspace/payment-section.module.css b/packages/cloud/app/src/component/workspace/payment-section.module.css
deleted file mode 100644
index ea8e2ed42..000000000
--- a/packages/cloud/app/src/component/workspace/payment-section.module.css
+++ /dev/null
@@ -1,72 +0,0 @@
-.root {
- [data-slot="payments-table"] {
- overflow-x: auto;
- }
-
- [data-slot="payments-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="payment-date"] {
- color: var(--color-text);
- }
-
- &[data-slot="payment-id"] {
- font-family: var(--font-mono);
- font-weight: 400;
- color: var(--color-text-muted);
- max-width: 200px;
- word-break: break-word;
- }
-
- &[data-slot="payment-amount"] {
- color: var(--color-text);
- }
- }
-
- tbody tr {
- &:last-child td {
- border-bottom: none;
- }
- }
-
- @media (max-width: 40rem) {
- th,
- td {
- padding: var(--space-2) var(--space-3);
- font-size: var(--font-size-xs);
- }
-
- th {
- &:nth-child(2) /* Payment ID */ {
- display: none;
- }
- }
-
- td {
- &:nth-child(2) /* Payment ID */ {
- display: none;
- }
- }
- }
- }
-}
diff --git a/packages/cloud/app/src/component/workspace/payment-section.tsx b/packages/cloud/app/src/component/workspace/payment-section.tsx
deleted file mode 100644
index 8cdceebc3..000000000
--- a/packages/cloud/app/src/component/workspace/payment-section.tsx
+++ /dev/null
@@ -1,113 +0,0 @@
-import { Billing } from "@opencode/cloud-core/billing.js"
-import { query, action, useParams, createAsync, useAction } from "@solidjs/router"
-import { For } from "solid-js"
-import { withActor } from "~/context/auth.withActor"
-import { formatDateUTC, formatDateForTable } from "./common"
-import styles from "./payment-section.module.css"
-
-const getPaymentsInfo = query(async (workspaceID: string) => {
- "use server"
- return withActor(async () => {
- return await Billing.payments()
- }, workspaceID)
-}, "payment.list")
-
-const downloadReceipt = action(async (workspaceID: string, paymentID: string) => {
- "use server"
- return withActor(() => Billing.generateReceiptUrl({ paymentID }), workspaceID)
-}, "receipt.download")
-
-export function PaymentSection() {
- const params = useParams()
- // ORIGINAL CODE - COMMENTED OUT FOR TESTING
- const payments = createAsync(() => getPaymentsInfo(params.id))
- const downloadReceiptAction = useAction(downloadReceipt)
-
- // DUMMY DATA FOR TESTING
- // const payments = () => [
- // {
- // id: "pi_3QK1x2FT9vXn4A6r1234567890",
- // paymentID: "pi_3QK1x2FT9vXn4A6r1234567890",
- // timeCreated: new Date(Date.now() - 86400000 * 1).toISOString(), // 1 day ago
- // amount: 2100000000, // $21.00 ($20 + $1 fee)
- // },
- // {
- // id: "pi_3QJ8k7FT9vXn4A6r0987654321",
- // paymentID: "pi_3QJ8k7FT9vXn4A6r0987654321",
- // timeCreated: new Date(Date.now() - 86400000 * 15).toISOString(), // 15 days ago
- // amount: 2100000000, // $21.00
- // },
- // {
- // id: "pi_3QI5m1FT9vXn4A6r5678901234",
- // paymentID: "pi_3QI5m1FT9vXn4A6r5678901234",
- // timeCreated: new Date(Date.now() - 86400000 * 32).toISOString(), // 32 days ago
- // amount: 2100000000, // $21.00
- // },
- // {
- // id: "pi_3QH2n9FT9vXn4A6r3456789012",
- // paymentID: "pi_3QH2n9FT9vXn4A6r3456789012",
- // timeCreated: new Date(Date.now() - 86400000 * 47).toISOString(), // 47 days ago
- // amount: 2100000000, // $21.00
- // },
- // {
- // id: "pi_3QG7p4FT9vXn4A6r7890123456",
- // paymentID: "pi_3QG7p4FT9vXn4A6r7890123456",
- // timeCreated: new Date(Date.now() - 86400000 * 63).toISOString(), // 63 days ago
- // amount: 2100000000, // $21.00
- // },
- // ]
-
- return (
- payments() &&
- payments()!.length > 0 && (
- <section class={styles.root}>
- <div data-slot="section-title">
- <h2>Payments History</h2>
- <p>Recent payment transactions.</p>
- </div>
- <div data-slot="payments-table">
- <table data-slot="payments-table-element">
- <thead>
- <tr>
- <th>Date</th>
- <th>Payment ID</th>
- <th>Amount</th>
- <th>Receipt</th>
- </tr>
- </thead>
- <tbody>
- <For each={payments()!}>
- {(payment) => {
- const date = new Date(payment.timeCreated)
- return (
- <tr>
- <td data-slot="payment-date" title={formatDateUTC(date)}>
- {formatDateForTable(date)}
- </td>
- <td data-slot="payment-id">{payment.id}</td>
- <td data-slot="payment-amount">${((payment.amount ?? 0) / 100000000).toFixed(2)}</td>
- <td data-slot="payment-receipt">
- <button
- onClick={async () => {
- const receiptUrl = await downloadReceiptAction(params.id, payment.paymentID!)
- if (receiptUrl) {
- window.open(receiptUrl, "_blank")
- }
- }}
- data-slot="receipt-button"
- style="cursor: pointer;"
- >
- view
- </button>
- </td>
- </tr>
- )
- }}
- </For>
- </tbody>
- </table>
- </div>
- </section>
- )
- )
-}
diff --git a/packages/cloud/app/src/component/workspace/usage-section.module.css b/packages/cloud/app/src/component/workspace/usage-section.module.css
deleted file mode 100644
index 1a772ba87..000000000
--- a/packages/cloud/app/src/component/workspace/usage-section.module.css
+++ /dev/null
@@ -1,88 +0,0 @@
-.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);
- }
- }
-
- [data-slot="usage-table"] {
- overflow-x: auto;
- }
-
- [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);
- 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;
- }
- }
-
- @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;
- }
- }
- }
- }
-}
diff --git a/packages/cloud/app/src/component/workspace/usage-section.tsx b/packages/cloud/app/src/component/workspace/usage-section.tsx
deleted file mode 100644
index 5d3d3b6c3..000000000
--- a/packages/cloud/app/src/component/workspace/usage-section.tsx
+++ /dev/null
@@ -1,128 +0,0 @@
-import { Billing } from "@opencode/cloud-core/billing.js"
-import { query, useParams, createAsync } from "@solidjs/router"
-import { createMemo, For, Show } from "solid-js"
-import { formatDateUTC, formatDateForTable } from "./common"
-import { withActor } from "~/context/auth.withActor"
-import styles from "./usage-section.module.css"
-
-const getUsageInfo = query(async (workspaceID: string) => {
- "use server"
- return withActor(async () => {
- return await Billing.usages()
- }, workspaceID)
-}, "usage.list")
-
-export function UsageSection() {
- const params = useParams()
- // ORIGINAL CODE - COMMENTED OUT FOR TESTING
- const usage = createAsync(() => getUsageInfo(params.id))
-
- // 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
- // },
- // ]
-
- return (
- <section class={styles.root}>
- <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}
- fallback={
- <div data-component="empty-state">
- <p>Make your first API call to get started.</p>
- </div>
- }
- >
- <table data-slot="usage-table-element">
- <thead>
- <tr>
- <th>Date</th>
- <th>Model</th>
- <th>Input</th>
- <th>Output</th>
- <th>Cost</th>
- </tr>
- </thead>
- <tbody>
- <For each={usage()!}>
- {(usage) => {
- const date = createMemo(() => new Date(usage.timeCreated))
- return (
- <tr>
- <td data-slot="usage-date" title={formatDateUTC(date())}>
- {formatDateForTable(date())}
- </td>
- <td data-slot="usage-model">{usage.model}</td>
- <td data-slot="usage-tokens">{usage.inputTokens}</td>
- <td data-slot="usage-tokens">{usage.outputTokens}</td>
- <td data-slot="usage-cost">${((usage.cost ?? 0) / 100000000).toFixed(4)}</td>
- </tr>
- )
- }}
- </For>
- </tbody>
- </table>
- </Show>
- </div>
- </section>
- )
-}