mirror of
https://github.com/aljazceru/opencode.git
synced 2025-12-22 18:24:21 +01:00
ignore: zen
This commit is contained in:
@@ -1,6 +1,71 @@
|
|||||||
[data-page="workspace"] {
|
[data-page="workspace"] {
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
|
||||||
|
/* Common elements */
|
||||||
|
button {
|
||||||
|
padding: var(--space-3) var(--space-4);
|
||||||
|
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-sans);
|
||||||
|
font-weight: 500;
|
||||||
|
text-transform: uppercase;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-surface-hover);
|
||||||
|
border-color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-bg);
|
||||||
|
border-color: var(--color-border);
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-color="primary"] {
|
||||||
|
background-color: var(--color-primary);
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
color: var(--color-primary-text);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-primary-hover);
|
||||||
|
border-color: var(--color-primary-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-color="ghost"] {
|
||||||
|
background-color: transparent;
|
||||||
|
border-color: transparent;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-surface-hover);
|
||||||
|
border-color: var(--color-border);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--color-text);
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: var(--space-0-75);
|
||||||
|
text-decoration-thickness: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Workspace Header */
|
/* Workspace Header */
|
||||||
[data-component="workspace-header"] {
|
[data-component="workspace-header"] {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
|
|||||||
@@ -11,8 +11,7 @@ const getUserInfo = query(async (workspaceID: string) => {
|
|||||||
"use server"
|
"use server"
|
||||||
return withActor(async () => {
|
return withActor(async () => {
|
||||||
const actor = Actor.assert("user")
|
const actor = Actor.assert("user")
|
||||||
const user = await User.fromID(actor.properties.userID)
|
return await User.fromID(actor.properties.userID)
|
||||||
return { user }
|
|
||||||
}, workspaceID)
|
}, workspaceID)
|
||||||
}, "userInfo")
|
}, "userInfo")
|
||||||
|
|
||||||
@@ -44,7 +43,7 @@ export default function WorkspaceLayout(props: RouteSectionProps) {
|
|||||||
</A>
|
</A>
|
||||||
</div>
|
</div>
|
||||||
<div data-slot="header-actions">
|
<div data-slot="header-actions">
|
||||||
<span data-slot="user">{userInfo()?.user.email}</span>
|
<span data-slot="user">{userInfo()?.email}</span>
|
||||||
<form action={logout} method="post">
|
<form action={logout} method="post">
|
||||||
<button type="submit" formaction={logout}>
|
<button type="submit" formaction={logout}>
|
||||||
Logout
|
Logout
|
||||||
@@ -52,7 +51,7 @@ export default function WorkspaceLayout(props: RouteSectionProps) {
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div data-slot="content">{props.children}</div>
|
<div>{props.children}</div>
|
||||||
</main>
|
</main>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
/* Root container */
|
[data-page="workspace-[id]"] {
|
||||||
[data-slot="root"] {
|
|
||||||
max-width: 64rem;
|
max-width: 64rem;
|
||||||
padding: var(--space-10) var(--space-4);
|
padding: var(--space-10) var(--space-4);
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
@@ -28,6 +27,33 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--space-6);
|
gap: var(--space-6);
|
||||||
|
|
||||||
|
/* Section titles */
|
||||||
|
[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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
line-height: 1.4;
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
section:not(:last-child) {
|
section:not(:last-child) {
|
||||||
border-bottom: 1px solid var(--color-border);
|
border-bottom: 1px solid var(--color-border);
|
||||||
@@ -39,72 +65,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Common elements */
|
[data-component="empty-state"] {
|
||||||
button {
|
|
||||||
padding: var(--space-3) var(--space-4);
|
|
||||||
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-sans);
|
|
||||||
font-weight: 500;
|
|
||||||
text-transform: uppercase;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--color-surface-hover);
|
|
||||||
border-color: var(--color-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
transform: translateY(1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--color-bg);
|
|
||||||
border-color: var(--color-border);
|
|
||||||
transform: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-color="primary"] {
|
|
||||||
background-color: var(--color-primary);
|
|
||||||
border-color: var(--color-primary);
|
|
||||||
color: var(--color-primary-text);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--color-primary-hover);
|
|
||||||
border-color: var(--color-primary-hover);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-color="ghost"] {
|
|
||||||
background-color: transparent;
|
|
||||||
border-color: transparent;
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--color-surface-hover);
|
|
||||||
border-color: var(--color-border);
|
|
||||||
color: var(--color-text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: var(--color-text);
|
|
||||||
text-decoration: underline;
|
|
||||||
text-underline-offset: var(--space-0-75);
|
|
||||||
text-decoration-thickness: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="empty-state"] {
|
|
||||||
padding: var(--space-20) var(--space-6);
|
padding: var(--space-20) var(--space-6);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border: 1px dashed var(--color-border);
|
border: 1px dashed var(--color-border);
|
||||||
@@ -121,7 +82,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Title section */
|
/* Title section */
|
||||||
[data-slot="title-section"] {
|
[data-component="title-section"] {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
@@ -156,35 +117,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Section titles */
|
|
||||||
[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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
line-height: 1.4;
|
|
||||||
font-size: var(--font-size-sm);
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* API Keys Section */
|
/* API Keys Section */
|
||||||
[data-slot="api-keys-section"] {
|
[data-component="api-keys-section"] {
|
||||||
[data-slot="create-form"] {
|
[data-slot="create-form"] {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--space-3);
|
gap: var(--space-3);
|
||||||
@@ -304,7 +238,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Balance Section */
|
/* Balance Section */
|
||||||
[data-slot="balance-section"] {
|
[data-component="balance-section"] {
|
||||||
[data-slot="balance"] {
|
[data-slot="balance"] {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -324,7 +258,7 @@
|
|||||||
gap: var(--space-1);
|
gap: var(--space-1);
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
|
||||||
&.danger {
|
&[data-state="danger"] {
|
||||||
[data-slot="value"] {
|
[data-slot="value"] {
|
||||||
color: var(--color-danger);
|
color: var(--color-danger);
|
||||||
}
|
}
|
||||||
@@ -348,7 +282,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Payments Section */
|
/* Payments Section */
|
||||||
[data-slot="payments-section"] {
|
[data-component="payments-section"] {
|
||||||
[data-slot="payments-table"] {
|
[data-slot="payments-table"] {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
@@ -422,7 +356,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Usage Section */
|
/* Usage Section */
|
||||||
[data-slot="usage-section"] {
|
[data-component="usage-section"] {
|
||||||
[data-slot="usage-table"] {
|
[data-slot="usage-table"] {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,49 @@
|
|||||||
import "./[id].css"
|
import "./[id].css"
|
||||||
import { Billing } from "@opencode/cloud-core/billing.js"
|
import { Billing } from "@opencode/cloud-core/billing.js"
|
||||||
import { Key } from "@opencode/cloud-core/key.js"
|
import { Key } from "@opencode/cloud-core/key.js"
|
||||||
import { action, createAsync, query, useAction, useSubmission, json, useParams } from "@solidjs/router"
|
import {
|
||||||
|
json,
|
||||||
|
query,
|
||||||
|
action,
|
||||||
|
useParams,
|
||||||
|
useAction,
|
||||||
|
createAsync,
|
||||||
|
useSubmission,
|
||||||
|
} from "@solidjs/router"
|
||||||
import { createMemo, createSignal, For, Show } from "solid-js"
|
import { createMemo, createSignal, For, Show } from "solid-js"
|
||||||
import { withActor } from "~/context/auth.withActor"
|
import { withActor } from "~/context/auth.withActor"
|
||||||
import { IconCopy, IconCheck } from "~/component/icon"
|
import { IconCopy, IconCheck } from "~/component/icon"
|
||||||
import { User } from "@opencode/cloud-core/user.js"
|
|
||||||
import { Actor } from "@opencode/cloud-core/actor.js"
|
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(",", ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
/////////////////////////////////////
|
/////////////////////////////////////
|
||||||
// Keys related queries and actions
|
// Keys related queries and actions
|
||||||
/////////////////////////////////////
|
/////////////////////////////////////
|
||||||
|
|
||||||
|
|
||||||
const listKeys = query(async (workspaceID: string) => {
|
const listKeys = query(async (workspaceID: string) => {
|
||||||
"use server"
|
"use server"
|
||||||
return withActor(() => Key.list(), workspaceID)
|
return withActor(() => Key.list(), workspaceID)
|
||||||
@@ -38,187 +69,143 @@ const removeKey = action(async (workspaceID: string, id: string) => {
|
|||||||
// Billing related queries and actions
|
// Billing related queries and actions
|
||||||
/////////////////////////////////////
|
/////////////////////////////////////
|
||||||
|
|
||||||
const getBillingInfo = query(async (workspaceID: string) => {
|
const getBalanceInfo = query(async (workspaceID: string) => {
|
||||||
"use server"
|
"use server"
|
||||||
return withActor(async () => {
|
return withActor(async () => {
|
||||||
const actor = Actor.assert("user")
|
return await Billing.get()
|
||||||
const now = Date.now()
|
|
||||||
const [user, billing, payments, usage] = await Promise.all([
|
|
||||||
User.fromID(actor.properties.userID),
|
|
||||||
Billing.get(),
|
|
||||||
Billing.payments(),
|
|
||||||
Billing.usages(),
|
|
||||||
])
|
|
||||||
console.log("duration", Date.now() - now)
|
|
||||||
return { user, billing, payments, usage }
|
|
||||||
}, workspaceID)
|
}, workspaceID)
|
||||||
}, "billingInfo")
|
}, "balanceInfo")
|
||||||
|
|
||||||
|
const getUsageInfo = query(async (workspaceID: string) => {
|
||||||
|
"use server"
|
||||||
|
return withActor(async () => {
|
||||||
|
return await Billing.usages()
|
||||||
|
}, workspaceID)
|
||||||
|
}, "usageInfo")
|
||||||
|
|
||||||
|
const getPaymentsInfo = query(async (workspaceID: string) => {
|
||||||
|
"use server"
|
||||||
|
return withActor(async () => {
|
||||||
|
return await Billing.payments()
|
||||||
|
}, workspaceID)
|
||||||
|
}, "paymentsInfo")
|
||||||
|
|
||||||
const createCheckoutUrl = action(async (workspaceID: string, successUrl: string, cancelUrl: string) => {
|
const createCheckoutUrl = action(async (workspaceID: string, successUrl: string, cancelUrl: string) => {
|
||||||
"use server"
|
"use server"
|
||||||
return withActor(() => Billing.generateCheckoutUrl({ successUrl, cancelUrl }), workspaceID)
|
return withActor(() => Billing.generateCheckoutUrl({ successUrl, cancelUrl }), workspaceID)
|
||||||
}, "checkoutUrl")
|
}, "checkoutUrl")
|
||||||
|
|
||||||
const createPortalUrl = action(async (workspaceID: string, returnUrl: string) => {
|
// const createPortalUrl = action(async (workspaceID: string, returnUrl: string) => {
|
||||||
"use server"
|
// "use server"
|
||||||
return withActor(() => Billing.generatePortalUrl({ returnUrl }), workspaceID)
|
// return withActor(() => Billing.generatePortalUrl({ returnUrl }), workspaceID)
|
||||||
}, "portalUrl")
|
// }, "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"),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
export default function() {
|
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
|
|
||||||
/////////////////
|
|
||||||
// Keys section
|
|
||||||
/////////////////
|
|
||||||
const keys = createAsync(() => listKeys(params.id))
|
const keys = createAsync(() => listKeys(params.id))
|
||||||
const createKeyAction = useAction(createKey)
|
// const keys = () => dummyKeys
|
||||||
const removeKeyAction = useAction(removeKey)
|
const [showForm, setShowForm] = createSignal(false)
|
||||||
const createKeySubmission = useSubmission(createKey)
|
const [name, setName] = createSignal("")
|
||||||
const [showCreateForm, setShowCreateForm] = createSignal(false)
|
const removeAction = useAction(removeKey)
|
||||||
const [keyName, setKeyName] = createSignal("")
|
const createAction = useAction(createKey)
|
||||||
const [copiedKeyId, setCopiedKeyId] = createSignal<string | null>(null)
|
const createSubmission = useSubmission(createKey)
|
||||||
|
const [copiedId, setCopiedId] = createSignal<string | null>(null)
|
||||||
|
|
||||||
const formatDate = (date: Date) => {
|
function formatKey(key: string) {
|
||||||
return date.toLocaleDateString()
|
|
||||||
}
|
|
||||||
|
|
||||||
const 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(",", ",")
|
|
||||||
}
|
|
||||||
|
|
||||||
const 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatKey = (key: string) => {
|
|
||||||
if (key.length <= 11) return key
|
if (key.length <= 11) return key
|
||||||
return `${key.slice(0, 7)}...${key.slice(-4)}`
|
return `${key.slice(0, 7)}...${key.slice(-4)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const copyToClipboard = async (text: string) => {
|
async function handleCreateKey() {
|
||||||
try {
|
if (!name().trim()) return
|
||||||
await navigator.clipboard.writeText(text)
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to copy to clipboard:", error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await createKeyAction(params.id, keyName().trim())
|
await createAction(params.id, name().trim())
|
||||||
setKeyName("")
|
setName("")
|
||||||
setShowCreateForm(false)
|
setShowForm(false)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to create API key:", error)
|
console.error("Failed to create API key:", error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDeleteKey = async (keyId: string) => {
|
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?")) {
|
if (!confirm("Are you sure you want to delete this API key?")) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await removeKeyAction(params.id, keyId)
|
await removeAction(params.id, keyId)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to delete API key:", error)
|
console.error("Failed to delete API key:", error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/////////////////
|
|
||||||
// Billing section
|
|
||||||
/////////////////
|
|
||||||
const billingInfo = createAsync(() => getBillingInfo(params.id))
|
|
||||||
const createCheckoutUrlAction = useAction(createCheckoutUrl)
|
|
||||||
const createCheckoutUrlSubmission = useSubmission(createCheckoutUrl)
|
|
||||||
|
|
||||||
const handleBuyCredits = async () => {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-slot="root">
|
<section data-component="api-keys-section">
|
||||||
{/* Title */}
|
|
||||||
<section data-slot="title-section">
|
|
||||||
<h1>Zen</h1>
|
|
||||||
<p>
|
|
||||||
Curated list of models provided by opencode. <a href="/docs/zen">Learn more</a>.
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div data-slot="sections">
|
|
||||||
{/* API Keys Section */}
|
|
||||||
<section data-slot="api-keys-section">
|
|
||||||
<div data-slot="section-title">
|
<div data-slot="section-title">
|
||||||
<h2>API Keys</h2>
|
<h2>API Keys</h2>
|
||||||
<p>Manage your API keys for accessing opencode services.</p>
|
<p>Manage your API keys for accessing opencode services.</p>
|
||||||
</div>
|
</div>
|
||||||
<Show
|
<Show
|
||||||
when={!showCreateForm()}
|
when={!showForm()}
|
||||||
fallback={
|
fallback={
|
||||||
<div data-slot="create-form">
|
<div data-slot="create-form">
|
||||||
<input
|
<input
|
||||||
data-component="input"
|
data-component="input"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Enter key name"
|
placeholder="Enter key name"
|
||||||
value={keyName()}
|
value={name()}
|
||||||
onInput={(e) => setKeyName(e.currentTarget.value)}
|
onInput={(e) => setName(e.currentTarget.value)}
|
||||||
onKeyPress={(e) => e.key === "Enter" && handleCreateKey()}
|
onKeyPress={(e) => e.key === "Enter" && handleCreateKey()}
|
||||||
/>
|
/>
|
||||||
<div data-slot="form-actions">
|
<div data-slot="form-actions">
|
||||||
<button
|
<button
|
||||||
data-color="ghost"
|
data-color="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowCreateForm(false)
|
setShowForm(false)
|
||||||
setKeyName("")
|
setName("")
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
data-color="primary"
|
data-color="primary"
|
||||||
disabled={createKeySubmission.pending || !keyName().trim()}
|
disabled={createSubmission.pending || !name().trim()}
|
||||||
onClick={handleCreateKey}
|
onClick={handleCreateKey}
|
||||||
>
|
>
|
||||||
{createKeySubmission.pending ? "Creating..." : "Create"}
|
{createSubmission.pending ? "Creating..." : "Create"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -228,7 +215,7 @@ export default function() {
|
|||||||
data-color="primary"
|
data-color="primary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
console.log("clicked")
|
console.log("clicked")
|
||||||
setShowCreateForm(true)
|
setShowForm(true)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Create API Key
|
Create API Key
|
||||||
@@ -238,7 +225,7 @@ export default function() {
|
|||||||
<Show
|
<Show
|
||||||
when={keys()?.length}
|
when={keys()?.length}
|
||||||
fallback={
|
fallback={
|
||||||
<div data-slot="empty-state">
|
<div data-component="empty-state">
|
||||||
<p>Create an opencode Gateway API key</p>
|
<p>Create an opencode Gateway API key</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -261,7 +248,7 @@ export default function() {
|
|||||||
<div onClick={() => copyKeyToClipboard(key.key, key.id)} title="Click to copy API key">
|
<div onClick={() => copyKeyToClipboard(key.key, key.id)} title="Click to copy API key">
|
||||||
<span>{formatKey(key.key)}</span>
|
<span>{formatKey(key.key)}</span>
|
||||||
<Show
|
<Show
|
||||||
when={copiedKeyId() === key.id}
|
when={copiedId() === key.id}
|
||||||
fallback={<IconCopy style={{ width: "14px", height: "14px" }} />}
|
fallback={<IconCopy style={{ width: "14px", height: "14px" }} />}
|
||||||
>
|
>
|
||||||
<IconCheck style={{ width: "14px", height: "14px" }} />
|
<IconCheck style={{ width: "14px", height: "14px" }} />
|
||||||
@@ -284,9 +271,32 @@ export default function() {
|
|||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
{/* Balance Section */}
|
function BalanceSection() {
|
||||||
<section data-slot="balance-section">
|
const params = useParams()
|
||||||
|
const dummyBalanceInfo = { balance: 2500000000 } // $25.00 in cents
|
||||||
|
|
||||||
|
const balanceInfo = createAsync(() => getBalanceInfo(params.id))
|
||||||
|
// const balanceInfo = () => dummyBalanceInfo
|
||||||
|
const createCheckoutUrlAction = useAction(createCheckoutUrl)
|
||||||
|
const createCheckoutUrlSubmission = useSubmission(createCheckoutUrl)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section data-component="balance-section">
|
||||||
<div data-slot="section-title">
|
<div data-slot="section-title">
|
||||||
<h2>Balance</h2>
|
<h2>Balance</h2>
|
||||||
<p>Add credits to your account.</p>
|
<p>Add credits to your account.</p>
|
||||||
@@ -294,17 +304,15 @@ export default function() {
|
|||||||
<div data-slot="balance">
|
<div data-slot="balance">
|
||||||
<div
|
<div
|
||||||
data-slot="amount"
|
data-slot="amount"
|
||||||
classList={{
|
data-state={(() => {
|
||||||
danger: (() => {
|
const balanceStr = ((balanceInfo()?.balance ?? 0) / 100000000).toFixed(2)
|
||||||
const balanceStr = ((billingInfo()?.billing?.balance ?? 0) / 100000000).toFixed(2)
|
return balanceStr === "0.00" || balanceStr === "-0.00" ? "danger" : undefined
|
||||||
return balanceStr === "0.00" || balanceStr === "-0.00"
|
})()}
|
||||||
})(),
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<span data-slot="currency">$</span>
|
<span data-slot="currency">$</span>
|
||||||
<span data-slot="value">
|
<span data-slot="value">
|
||||||
{(() => {
|
{(() => {
|
||||||
const balanceStr = ((billingInfo()?.billing?.balance ?? 0) / 100000000).toFixed(2)
|
const balanceStr = ((balanceInfo()?.balance ?? 0) / 100000000).toFixed(2)
|
||||||
return balanceStr === "-0.00" ? "0.00" : balanceStr
|
return balanceStr === "-0.00" ? "0.00" : balanceStr
|
||||||
})()}
|
})()}
|
||||||
</span>
|
</span>
|
||||||
@@ -314,18 +322,59 @@ export default function() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
{/* Usage Section */}
|
function UsageSection() {
|
||||||
<section data-slot="usage-section">
|
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">
|
<div data-slot="section-title">
|
||||||
<h2>Usage History</h2>
|
<h2>Usage History</h2>
|
||||||
<p>Recent API usage and costs.</p>
|
<p>Recent API usage and costs.</p>
|
||||||
</div>
|
</div>
|
||||||
<div data-slot="usage-table">
|
<div data-slot="usage-table">
|
||||||
<Show
|
<Show
|
||||||
when={billingInfo() && billingInfo()!.usage.length > 0}
|
when={usage() && usage()!.length > 0}
|
||||||
fallback={
|
fallback={
|
||||||
<div data-slot="empty-state">
|
<div data-component="empty-state">
|
||||||
<p>Make your first API call to get started.</p>
|
<p>Make your first API call to get started.</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -341,7 +390,7 @@ export default function() {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<For each={billingInfo()!.usage}>
|
<For each={usage()!}>
|
||||||
{(usage) => {
|
{(usage) => {
|
||||||
const date = createMemo(() => new Date(usage.timeCreated))
|
const date = createMemo(() => new Date(usage.timeCreated))
|
||||||
return (
|
return (
|
||||||
@@ -362,10 +411,29 @@ export default function() {
|
|||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
{/* Payments Section */}
|
function PaymentsSection() {
|
||||||
<Show when={billingInfo() && billingInfo()!.payments.length > 0}>
|
const params = useParams()
|
||||||
<section data-slot="payments-section">
|
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() && payments()!.length > 0 && (
|
||||||
|
<section data-component="payments-section">
|
||||||
<div data-slot="section-title">
|
<div data-slot="section-title">
|
||||||
<h2>Payments History</h2>
|
<h2>Payments History</h2>
|
||||||
<p>Recent payment transactions.</p>
|
<p>Recent payment transactions.</p>
|
||||||
@@ -380,7 +448,7 @@ export default function() {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<For each={billingInfo()!.payments}>
|
<For each={payments()!}>
|
||||||
{(payment) => {
|
{(payment) => {
|
||||||
const date = new Date(payment.timeCreated)
|
const date = new Date(payment.timeCreated)
|
||||||
return (
|
return (
|
||||||
@@ -398,8 +466,24 @@ export default function() {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</Show>
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function() {
|
||||||
|
return (
|
||||||
|
<div data-page="workspace-[id]">
|
||||||
|
<section data-component="title-section">
|
||||||
|
<h1>Zen</h1>
|
||||||
|
<p>
|
||||||
|
Curated list of models provided by opencode. <a target="_blank" href="/docs/zen">Learn more</a>.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div data-slot="sections">
|
||||||
|
<KeysSection />
|
||||||
|
<BalanceSection />
|
||||||
|
<UsageSection />
|
||||||
|
<PaymentsSection />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user