Dax Raad 5 месяцев назад
Родитель
Сommit
79c73267cf
1 измененных файлов с 187 добавлено и 289 удалено
  1. 187 289
      cloud/app/src/routes/workspace/[id].tsx

+ 187 - 289
cloud/app/src/routes/workspace/[id].tsx

@@ -2,9 +2,10 @@ import "./[id].css"
 import { Billing } from "@opencode/cloud-core/billing.js"
 import { Key } from "@opencode/cloud-core/key.js"
 import { json, query, action, useParams, useAction, createAsync, useSubmission } from "@solidjs/router"
-import { createMemo, createSignal, For, Show } from "solid-js"
+import { createEffect, createMemo, createSignal, For, Show } from "solid-js"
 import { withActor } from "~/context/auth.withActor"
 import { IconCopy, IconCheck } from "~/component/icon"
+import { createStore } from "solid-js/store"
 
 function formatDateForTable(date: Date) {
   const options: Intl.DateTimeFormatOptions = {
@@ -41,16 +42,24 @@ const listKeys = query(async (workspaceID: string) => {
   return withActor(() => Key.list(), workspaceID)
 }, "key.list")
 
-const createKey = action(async (workspaceID: string, name: string) => {
+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(
     withActor(() => Key.create({ name }), workspaceID),
     { revalidate: listKeys.key },
   )
 }, "key.create")
 
-const removeKey = action(async (workspaceID: string, id: string) => {
+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(
     withActor(() => Key.remove({ id }), workspaceID),
     { revalidate: listKeys.key },
@@ -92,127 +101,22 @@ const createCheckoutUrl = action(async (workspaceID: string, successUrl: string,
 //   return withActor(() => Billing.generatePortalUrl({ returnUrl }), workspaceID)
 // }, "portalUrl")
 
-function KeysSection() {
-  // Dummy data for testing
-  const dummyKeys = [
-    {
-      id: "key_1",
-      name: "Development API Key",
-      key: "oc_dev_1234567890abcdef1234567890abcdef12345678",
-      timeCreated: new Date("2024-01-15T10:30:00Z"),
-    },
-    {
-      id: "key_2",
-      name: "Production API Key",
-      key: "oc_prod_abcdef1234567890abcdef1234567890abcdef12",
-      timeCreated: new Date("2024-02-01T14:22:00Z"),
-    },
-    {
-      id: "key_3",
-      name: "Testing Environment",
-      key: "oc_test_9876543210fedcba9876543210fedcba98765432",
-      timeCreated: new Date("2024-02-10T09:15:00Z"),
-    },
-  ]
-
+function KeySection() {
   const params = useParams()
   const keys = createAsync(() => listKeys(params.id))
-  // const keys = () => dummyKeys
-  const [showForm, setShowForm] = createSignal(false)
-  const [name, setName] = createSignal("")
-  const removeAction = useAction(removeKey)
-  const createAction = useAction(createKey)
-  const createSubmission = useSubmission(createKey)
-  const [copiedId, setCopiedId] = createSignal<string | null>(null)
 
   function formatKey(key: string) {
     if (key.length <= 11) return key
     return `${key.slice(0, 7)}...${key.slice(-4)}`
   }
 
-  async function handleCreateKey() {
-    if (!name().trim()) return
-
-    try {
-      await createAction(params.id, name().trim())
-      setName("")
-      setShowForm(false)
-    } catch (error) {
-      console.error("Failed to create API key:", error)
-    }
-  }
-
-  async function copyKeyToClipboard(text: string, keyId: string) {
-    try {
-      await navigator.clipboard.writeText(text)
-      setCopiedId(keyId)
-      setTimeout(() => setCopiedId(null), 1500)
-    } catch (error) {
-      console.error("Failed to copy to clipboard:", error)
-    }
-  }
-
-  async function handleDeleteKey(keyId: string) {
-    if (!confirm("Are you sure you want to delete this API key?")) {
-      return
-    }
-
-    try {
-      await removeAction(params.id, keyId)
-    } catch (error) {
-      console.error("Failed to delete API key:", error)
-    }
-  }
-
   return (
     <section data-component="api-keys-section">
       <div data-slot="section-title">
         <h2>API Keys</h2>
         <p>Manage your API keys for accessing opencode services.</p>
       </div>
-      <Show
-        when={!showForm()}
-        fallback={
-          <div data-slot="create-form">
-            <input
-              data-component="input"
-              type="text"
-              placeholder="Enter key name"
-              value={name()}
-              onInput={(e) => setName(e.currentTarget.value)}
-              onKeyPress={(e) => e.key === "Enter" && handleCreateKey()}
-            />
-            <div data-slot="form-actions">
-              <button
-                data-color="ghost"
-                onClick={() => {
-                  setShowForm(false)
-                  setName("")
-                }}
-              >
-                Cancel
-              </button>
-              <button
-                data-color="primary"
-                disabled={createSubmission.pending || !name().trim()}
-                onClick={handleCreateKey}
-              >
-                {createSubmission.pending ? "Creating..." : "Create"}
-              </button>
-            </div>
-          </div>
-        }
-      >
-        <button
-          data-color="primary"
-          onClick={() => {
-            console.log("clicked")
-            setShowForm(true)
-          }}
-        >
-          Create API Key
-        </button>
-      </Show>
+      <KeyCreateForm />
       <div data-slot="api-keys-table">
         <Show
           when={keys()?.length}
@@ -233,35 +137,42 @@ function KeysSection() {
             </thead>
             <tbody>
               <For each={keys()!}>
-                {(key) => (
-                  <tr>
-                    <td data-slot="key-name">{key.name}</td>
-                    <td data-slot="key-value">
-                      <button
-                        data-color="ghost"
-                        disabled={copiedId() === key.id}
-                        onClick={() => copyKeyToClipboard(key.key, key.id)}
-                        title="Copy API key"
-                      >
-                        <span>{formatKey(key.key)}</span>
-                        <Show
-                          when={copiedId() === key.id}
-                          fallback={<IconCopy style={{ width: "14px", height: "14px" }} />}
+                {(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"
                         >
-                          <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">
-                      <button data-color="ghost" onClick={() => handleDeleteKey(key.id)} title="Delete API key">
-                        Delete
-                      </button>
-                    </td>
-                  </tr>
-                )}
+                          <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>
@@ -271,27 +182,61 @@ function KeysSection() {
   )
 }
 
-function BalanceSection() {
+function KeyCreateForm() {
   const params = useParams()
-  const dummyBalanceInfo = { balance: 2500000000 } // $25.00 in cents
+  const submission = useSubmission(createKey)
+  const [store, setStore] = createStore({
+    show: false,
+  })
 
-  const balanceInfo = createAsync(() => getBalanceInfo(params.id))
-  // const balanceInfo = () => dummyBalanceInfo
-  const createCheckoutUrlAction = useAction(createCheckoutUrl)
-  const createCheckoutUrlSubmission = useSubmission(createCheckoutUrl)
+  let input: HTMLInputElement
 
-  async function handleBuyCredits() {
-    try {
-      const baseUrl = window.location.href
-      const checkoutUrl = await createCheckoutUrlAction(params.id, baseUrl, baseUrl)
-      if (checkoutUrl) {
-        window.location.href = checkoutUrl
-      }
-    } catch (error) {
-      console.error("Failed to get checkout URL:", error)
+  createEffect(() => {
+    if (!submission.pending && submission.result) {
+      hide()
     }
+  })
+
+  function show() {
+    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">
+        <input ref={(r) => (input = r)} data-component="input" name="name" type="text" placeholder="Enter key name" />
+        <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>
+  )
+}
+
+function BalanceSection() {
+  const params = useParams()
+  const balanceInfo = createAsync(() => getBalanceInfo(params.id))
+  const createCheckoutUrlAction = useAction(createCheckoutUrl)
+  const createCheckoutUrlSubmission = useSubmission(createCheckoutUrl)
+
   return (
     <section data-component="balance-section">
       <div data-slot="section-title">
@@ -314,7 +259,17 @@ function BalanceSection() {
             })()}
           </span>
         </div>
-        <button data-color="primary" disabled={createCheckoutUrlSubmission.pending} onClick={handleBuyCredits}>
+        <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..." : "Buy Credits"}
         </button>
       </div>
@@ -324,43 +279,8 @@ function BalanceSection() {
 
 function UsageSection() {
   const params = useParams()
-  const dummyUsage = [
-    {
-      id: "usage_1",
-      model: "claude-3-sonnet-20240229",
-      inputTokens: 1250,
-      outputTokens: 890,
-      cost: 125000000, // $1.25 in cents
-      timeCreated: "2024-02-10T15:30:00Z",
-    },
-    {
-      id: "usage_2",
-      model: "gpt-4-turbo-preview",
-      inputTokens: 2100,
-      outputTokens: 1456,
-      cost: 340000000, // $3.40 in cents
-      timeCreated: "2024-02-09T09:45:00Z",
-    },
-    {
-      id: "usage_3",
-      model: "claude-3-haiku-20240307",
-      inputTokens: 850,
-      outputTokens: 620,
-      cost: 45000000, // $0.45 in cents
-      timeCreated: "2024-02-08T13:22:00Z",
-    },
-    {
-      id: "usage_4",
-      model: "gpt-3.5-turbo",
-      inputTokens: 1800,
-      outputTokens: 1200,
-      cost: 85000000, // $0.85 in cents
-      timeCreated: "2024-02-07T11:15:00Z",
-    },
-  ]
-
   const usage = createAsync(() => getUsageInfo(params.id))
-  // const usage = () => dummyUsage
+
   return (
     <section data-component="usage-section">
       <div data-slot="section-title">
@@ -411,23 +331,9 @@ function UsageSection() {
   )
 }
 
-function PaymentsSection() {
+function PaymentSection() {
   const params = useParams()
-  const dummyPayments = [
-    {
-      id: "pi_1234567890",
-      amount: 5000000000, // $50.00 in cents
-      timeCreated: "2024-02-01T10:00:00Z",
-    },
-    {
-      id: "pi_0987654321",
-      amount: 2500000000, // $25.00 in cents
-      timeCreated: "2024-01-15T14:30:00Z",
-    },
-  ]
-
   const payments = createAsync(() => getPaymentsInfo(params.id))
-  // const payments = () => dummyPayments
 
   return (
     payments() &&
@@ -471,97 +377,90 @@ function PaymentsSection() {
 
 function NewUserSection() {
   const params = useParams()
-  const keys = createAsync(() => listKeys(params.id))
   const [copiedKey, setCopiedKey] = createSignal(false)
-
-  async function copyKeyToClipboard(text: string) {
-    try {
-      await navigator.clipboard.writeText(text)
-      setCopiedKey(true)
-      setTimeout(() => setCopiedKey(false), 2000)
-    } catch (error) {
-      console.error("Failed to copy to clipboard:", error)
-    }
-  }
+  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 (
-    <div data-slot="new-user-sections">
-      <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>
+    <Show when={isNew()}>
+      <div data-slot="new-user-sections">
+        <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>
 
-      <div data-component="api-key-highlight">
-        <div data-slot="section-title">
-          <h2>Your API Key</h2>
-        </div>
+        <div data-component="api-key-highlight">
+          <div data-slot="section-title">
+            <h2>Your API Key</h2>
+          </div>
 
-        <Show when={keys()?.length}>
-          <div data-slot="key-display">
-            <div data-slot="key-container">
-              <code data-slot="key-value">{keys()![0].key}</code>
-              <button
-                data-color="primary"
-                disabled={copiedKey()}
-                onClick={() => copyKeyToClipboard(keys()![0].key)}
-                title="Copy API key"
-              >
-                <Show
-                  when={copiedKey()}
-                  fallback={
-                    <>
-                      <IconCopy style={{ width: "16px", height: "16px" }} /> Copy Key
-                    </>
-                  }
+          <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"
                 >
-                  <IconCheck style={{ width: "16px", height: "16px" }} /> Copied!
-                </Show>
-              </button>
+                  <Show
+                    when={copiedKey()}
+                    fallback={
+                      <>
+                        <IconCopy style={{ width: "16px", height: "16px" }} /> Copy Key
+                      </>
+                    }
+                  >
+                    <IconCheck style={{ width: "16px", height: "16px" }} /> Copied!
+                  </Show>
+                </button>
+              </div>
             </div>
-          </div>
-        </Show>
-      </div>
+          </Show>
+        </div>
 
-      <div data-component="next-steps">
-        <div data-slot="section-title">
-          <h2>Next Steps</h2>
+        <div data-component="next-steps">
+          <div data-slot="section-title">
+            <h2>Next Steps</h2>
+          </div>
+          <ol>
+            <li>Copy your API key above</li>
+            <li>
+              Run <code>opencode auth login</code> and select opencode
+            </li>
+            <li>Paste your API key when prompted</li>
+            <li>
+              Run <code>/models</code> to see available models
+            </li>
+          </ol>
         </div>
-        <ol>
-          <li>Copy your API key above</li>
-          <li>
-            Run <code>opencode auth login</code> and select opencode
-          </li>
-          <li>Paste your API key when prompted</li>
-          <li>
-            Run <code>/models</code> to see available models
-          </li>
-        </ol>
       </div>
-    </div>
+    </Show>
   )
 }
 
 export default function () {
-  const params = useParams()
-  const keys = createAsync(() => listKeys(params.id))
-  const usage = createAsync(() => getUsageInfo(params.id))
-
-  const isNewUser = createMemo(() => {
-    const keysList = keys()
-    const usageList = usage()
-    return keysList?.length === 1 && (!usageList || usageList.length === 0)
-  })
-
   return (
     <div data-page="workspace-[id]">
       <section data-component="title-section">
@@ -575,14 +474,13 @@ export default function () {
         </p>
       </section>
 
-      <Show when={!isNewUser()} fallback={<NewUserSection />}>
-        <div data-slot="sections">
-          <KeysSection />
-          <BalanceSection />
-          <UsageSection />
-          <PaymentsSection />
-        </div>
-      </Show>
+      <div data-slot="sections">
+        <NewUserSection />
+        <KeySection />
+        <BalanceSection />
+        <UsageSection />
+        <PaymentSection />
+      </div>
     </div>
   )
 }