diff options
| author | Jay V <[email protected]> | 2025-08-29 20:04:57 -0400 |
|---|---|---|
| committer | Jay V <[email protected]> | 2025-08-29 20:04:57 -0400 |
| commit | 9a330b4f0f5a24a26e859c7a56daeedb397c3c67 (patch) | |
| tree | 08459281eacdec12268330d28f532d6fa6125e52 /cloud/app/src | |
| parent | 25e53e090bec1f92fb6b2bbec52f7d1993836972 (diff) | |
| download | opencode-9a330b4f0f5a24a26e859c7a56daeedb397c3c67.tar.gz opencode-9a330b4f0f5a24a26e859c7a56daeedb397c3c67.zip | |
ignore: cloud keys section
Diffstat (limited to 'cloud/app/src')
| -rw-r--r-- | cloud/app/src/routes/workspace/[id].tsx | 111 | ||||
| -rw-r--r-- | cloud/app/src/routes/workspace/index.css | 123 |
2 files changed, 158 insertions, 76 deletions
diff --git a/cloud/app/src/routes/workspace/[id].tsx b/cloud/app/src/routes/workspace/[id].tsx index 07f4fc0d0..cf6c0bdd2 100644 --- a/cloud/app/src/routes/workspace/[id].tsx +++ b/cloud/app/src/routes/workspace/[id].tsx @@ -4,6 +4,7 @@ import { action, createAsync, revalidate, query, useAction, useSubmission, json import { createEffect, createSignal, For, onMount, Show } from "solid-js" import { getActor } from "~/context/auth" import { withActor } from "~/context/auth.withActor" +import { IconCopy, IconCheck } from "~/component/icon" import "./index.css" import { User } from "@opencode/cloud-core/user.js" import { Actor } from "@opencode/cloud-core/actor.js" @@ -142,7 +143,31 @@ const dummyPaymentData = [ }, ] -export default function () { +const dummyApiKeyData = [ + { + id: "key_1Ab2Cd3Ef4Gh5678", + name: "Production API", + key: "oc_live_sk_1Ab2Cd3Ef4Gh567890123456789012345678901234567890", + timeCreated: new Date("2025-01-28T14:32:00Z"), + timeUsed: new Date("2025-01-29T09:15:00Z"), + }, + { + id: "key_9Ij8Kl7Mn6Op5432", + name: "Development Key", + key: "oc_test_sk_9Ij8Kl7Mn6Op543210987654321098765432109876543210", + timeCreated: new Date("2025-01-25T09:18:00Z"), + timeUsed: null, + }, + { + id: "key_5Qr4St3Uv2Wx1098", + name: "CI/CD Pipeline", + key: "oc_live_sk_5Qr4St3Uv2Wx109876543210987654321098765432109876", + timeCreated: new Date("2025-01-20T16:45:00Z"), + timeUsed: new Date("2025-01-28T12:30:00Z"), + }, +] + +export default function() { const actor = createAsync(() => getActor()) onMount(() => { console.log("MOUNTED", actor()) @@ -157,6 +182,7 @@ export default function () { const createKeySubmission = useSubmission(createKey) const [showCreateForm, setShowCreateForm] = createSignal(false) const [keyName, setKeyName] = createSignal("") + const [copiedKeyId, setCopiedKeyId] = createSignal<string | null>(null) const formatDate = (date: Date) => { return date.toLocaleDateString() @@ -201,6 +227,16 @@ export default function () { } } + const copyKeyToClipboard = async (text: string, keyId: string) => { + try { + await navigator.clipboard.writeText(text) + setCopiedKeyId(keyId) + setTimeout(() => setCopiedKeyId(null), 1500) + } catch (error) { + console.error("Failed to copy to clipboard:", error) + } + } + const handleCreateKey = async () => { if (!keyName().trim()) return @@ -214,7 +250,7 @@ export default function () { } const handleDeleteKey = async (keyId: string) => { - if (!confirm("Are you sure you want to delete this API key? This action cannot be undone.")) { + if (!confirm("Are you sure you want to delete this API key?")) { return } @@ -291,7 +327,7 @@ export default function () { </section> {/* API Keys Section */} - <section data-slot="keys-section"> + <section data-slot="api-keys-section"> <div data-slot="section-title"> <h2>API Keys</h2> <p>Manage your API keys for accessing opencode services.</p> @@ -339,36 +375,55 @@ export default function () { Create API Key </button> </Show> - <div data-slot="key-list"> - <For - each={keys()} + <div data-slot="api-keys-table"> + <Show + when={dummyApiKeyData.length > 0} fallback={ <div data-slot="empty-state"> - <p>Create an API key to access opencode gateway</p> + <p>Create an opencode Gateway API key</p> </div> } > - {(key) => ( - <div data-slot="key-item"> - <div data-slot="key-info"> - <div data-slot="key-name">{key.name}</div> - <div data-slot="key-value">{formatKey(key.key)}</div> - <div data-slot="key-meta"> - Created: {formatDate(key.timeCreated)} - {key.timeUsed && ` • Last used: ${formatDate(key.timeUsed)}`} - </div> - </div> - <div data-slot="key-actions"> - <button color="ghost" onClick={() => copyToClipboard(key.key)} title="Copy API key"> - Copy - </button> - <button color="ghost" onClick={() => handleDeleteKey(key.id)} title="Delete API key"> - Delete - </button> - </div> - </div> - )} - </For> + <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={dummyApiKeyData}> + {/* Real data: keys() */} + {(key) => ( + <tr> + <td data-slot="key-name">{key.name}</td> + <td data-slot="key-value"> + <div onClick={() => copyKeyToClipboard(key.key, key.id)} title="Click to copy API key"> + <span>{formatKey(key.key)}</span> + <Show + when={copiedKeyId() === key.id} + fallback={<IconCopy style={{ width: "14px", height: "14px" }} />} + > + <IconCheck style={{ width: "14px", height: "14px" }} /> + </Show> + </div> + </td> + <td data-slot="key-date" title={formatDateUTC(key.timeCreated)}> + {formatDateForTable(key.timeCreated)} + </td> + <td data-slot="key-actions"> + <button color="ghost" onClick={() => handleDeleteKey(key.id)} title="Delete API key"> + Delete + </button> + </td> + </tr> + )} + </For> + </tbody> + </table> + </Show> </div> </section> diff --git a/cloud/app/src/routes/workspace/index.css b/cloud/app/src/routes/workspace/index.css index 0d04ea25b..887469e33 100644 --- a/cloud/app/src/routes/workspace/index.css +++ b/cloud/app/src/routes/workspace/index.css @@ -203,59 +203,84 @@ a { } } - [data-slot="key-list"] { - display: flex; - flex-direction: column; - gap: var(--space-2); + [data-slot="api-keys-table"] { + overflow-x: auto; } - [data-slot="key-item"] { - display: flex; - justify-content: space-between; - align-items: flex-start; - padding: var(--space-4); - background-color: var(--color-bg-surface); - border: 1px solid var(--color-border); - border-radius: var(--border-radius-sm); - gap: var(--space-4); + [data-slot="api-keys-table-element"] { + width: 100%; + border-collapse: collapse; + font-size: var(--font-size-sm); - @media (max-width: 30rem) { - flex-direction: column; - gap: var(--space-3); + thead { + border-bottom: 1px solid var(--color-border); } - } - [data-slot="key-info"] { - display: flex; - flex-direction: column; - gap: var(--space-1); - flex: 1; - } + th { + padding: var(--space-3) var(--space-4); + text-align: left; + font-weight: normal; + color: var(--color-text-muted); + text-transform: uppercase; + } - [data-slot="key-name"] { - font-size: var(--font-size-md); - font-weight: 500; - color: var(--color-text); - } + 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-value"] { - font-size: var(--font-size-xs); - font-family: var(--font-mono); - color: var(--color-text-secondary); - background-color: var(--color-bg); - padding: var(--space-1) var(--space-2); - border-radius: var(--border-radius-sm); - border: 1px solid var(--color-border-muted); - } + &[data-slot="key-name"] { + color: var(--color-text); + font-family: var(--font-sans); + font-weight: 500; + } - [data-slot="key-meta"] { - font-size: var(--font-size-xs); - color: var(--color-text-disabled); - } + &[data-slot="key-value"] { + font-family: var(--font-mono); - [data-slot="key-actions"] { - display: flex; - gap: var(--space-2); + div { + cursor: pointer; + display: flex; + align-items: center; + gap: var(--space-2); + } + } + + &[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; + } + } + } } } @@ -321,8 +346,9 @@ a { th { padding: var(--space-3) var(--space-4); text-align: left; - font-weight: 600; - color: var(--color-text-secondary); + font-weight: normal; + color: var(--color-text-muted); + text-transform: uppercase; } td { @@ -394,8 +420,9 @@ a { th { padding: var(--space-3) var(--space-4); text-align: left; - font-weight: 600; - color: var(--color-text-secondary); + font-weight: normal; + color: var(--color-text-muted); + text-transform: uppercase; } td { |
