summaryrefslogtreecommitdiffhomepage
path: root/cloud
diff options
context:
space:
mode:
authorJay V <[email protected]>2025-08-29 20:04:57 -0400
committerJay V <[email protected]>2025-08-29 20:04:57 -0400
commit9a330b4f0f5a24a26e859c7a56daeedb397c3c67 (patch)
tree08459281eacdec12268330d28f532d6fa6125e52 /cloud
parent25e53e090bec1f92fb6b2bbec52f7d1993836972 (diff)
downloadopencode-9a330b4f0f5a24a26e859c7a56daeedb397c3c67.tar.gz
opencode-9a330b4f0f5a24a26e859c7a56daeedb397c3c67.zip
ignore: cloud keys section
Diffstat (limited to 'cloud')
-rw-r--r--cloud/app/src/routes/workspace/[id].tsx111
-rw-r--r--cloud/app/src/routes/workspace/index.css123
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 {