summaryrefslogtreecommitdiffhomepage
path: root/packages/console/app/src
diff options
context:
space:
mode:
authorFrank <[email protected]>2025-09-28 19:55:38 -0400
committerFrank <[email protected]>2025-09-28 19:55:40 -0400
commit918739057d28ec440547bbe1299c1b6da9cf5485 (patch)
treef29060dc58ab35ac9a5ca193f99e601f5c8a357b /packages/console/app/src
parente3a7096e447d3e9b217f870da4623fab34298479 (diff)
downloadopencode-918739057d28ec440547bbe1299c1b6da9cf5485.tar.gz
opencode-918739057d28ec440547bbe1299c1b6da9cf5485.zip
wip: zen
Diffstat (limited to 'packages/console/app/src')
-rw-r--r--packages/console/app/src/routes/workspace/[id].tsx21
-rw-r--r--packages/console/app/src/routes/workspace/billing-section.module.css (renamed from packages/console/app/src/component/workspace/billing-section.module.css)0
-rw-r--r--packages/console/app/src/routes/workspace/billing-section.tsx (renamed from packages/console/app/src/component/workspace/billing-section.tsx)0
-rw-r--r--packages/console/app/src/routes/workspace/common.tsx (renamed from packages/console/app/src/component/workspace/common.tsx)0
-rw-r--r--packages/console/app/src/routes/workspace/key-section.module.css (renamed from packages/console/app/src/component/workspace/key-section.module.css)0
-rw-r--r--packages/console/app/src/routes/workspace/key-section.tsx (renamed from packages/console/app/src/component/workspace/key-section.tsx)0
-rw-r--r--packages/console/app/src/routes/workspace/member-section.module.css179
-rw-r--r--packages/console/app/src/routes/workspace/member-section.tsx189
-rw-r--r--packages/console/app/src/routes/workspace/monthly-limit-section.module.css (renamed from packages/console/app/src/component/workspace/monthly-limit-section.module.css)0
-rw-r--r--packages/console/app/src/routes/workspace/monthly-limit-section.tsx (renamed from packages/console/app/src/component/workspace/monthly-limit-section.tsx)0
-rw-r--r--packages/console/app/src/routes/workspace/new-user-section.module.css (renamed from packages/console/app/src/component/workspace/new-user-section.module.css)0
-rw-r--r--packages/console/app/src/routes/workspace/new-user-section.tsx (renamed from packages/console/app/src/component/workspace/new-user-section.tsx)0
-rw-r--r--packages/console/app/src/routes/workspace/payment-section.module.css (renamed from packages/console/app/src/component/workspace/payment-section.module.css)0
-rw-r--r--packages/console/app/src/routes/workspace/payment-section.tsx (renamed from packages/console/app/src/component/workspace/payment-section.tsx)0
-rw-r--r--packages/console/app/src/routes/workspace/usage-section.module.css (renamed from packages/console/app/src/component/workspace/usage-section.module.css)0
-rw-r--r--packages/console/app/src/routes/workspace/usage-section.tsx (renamed from packages/console/app/src/component/workspace/usage-section.tsx)0
16 files changed, 382 insertions, 7 deletions
diff --git a/packages/console/app/src/routes/workspace/[id].tsx b/packages/console/app/src/routes/workspace/[id].tsx
index af8a0dd63..5257a8e97 100644
--- a/packages/console/app/src/routes/workspace/[id].tsx
+++ b/packages/console/app/src/routes/workspace/[id].tsx
@@ -1,12 +1,16 @@
import "./[id].css"
-import { MonthlyLimitSection } from "~/component/workspace/monthly-limit-section"
-import { NewUserSection } from "~/component/workspace/new-user-section"
-import { BillingSection } from "~/component/workspace/billing-section"
-import { PaymentSection } from "~/component/workspace/payment-section"
-import { UsageSection } from "~/component/workspace/usage-section"
-import { KeySection } from "~/component/workspace/key-section"
+import { MonthlyLimitSection } from "./monthly-limit-section"
+import { NewUserSection } from "./new-user-section"
+import { BillingSection } from "./billing-section"
+import { PaymentSection } from "./payment-section"
+import { UsageSection } from "./usage-section"
+import { KeySection } from "./key-section"
+import { MemberSection } from "./member-section"
+import { Show } from "solid-js"
+import { useParams } from "@solidjs/router"
export default function () {
+ const params = useParams()
return (
<div data-page="workspace-[id]">
<section data-component="title-section">
@@ -23,6 +27,9 @@ export default function () {
<div data-slot="sections">
<NewUserSection />
<KeySection />
+ <Show when={isBeta(params.id)}>
+ <MemberSection />
+ </Show>
<BillingSection />
<MonthlyLimitSection />
<UsageSection />
@@ -36,6 +43,6 @@ export function isBeta(workspaceID: string) {
return [
"wrk_01K46JDFR0E75SG2Q8K172KF3Y", // production
"wrk_01K4NFRR5P7FSYWH88307B4DDS", // dev
- "wrk_01K4PJRKJ2WPQZN3FFYRV4673F", // frank
+ "wrk_01K68M8J1KK0PJ39H59B1EGHP6", // frank
].includes(workspaceID)
}
diff --git a/packages/console/app/src/component/workspace/billing-section.module.css b/packages/console/app/src/routes/workspace/billing-section.module.css
index 0bb5709cb..0bb5709cb 100644
--- a/packages/console/app/src/component/workspace/billing-section.module.css
+++ b/packages/console/app/src/routes/workspace/billing-section.module.css
diff --git a/packages/console/app/src/component/workspace/billing-section.tsx b/packages/console/app/src/routes/workspace/billing-section.tsx
index 02cf8433e..02cf8433e 100644
--- a/packages/console/app/src/component/workspace/billing-section.tsx
+++ b/packages/console/app/src/routes/workspace/billing-section.tsx
diff --git a/packages/console/app/src/component/workspace/common.tsx b/packages/console/app/src/routes/workspace/common.tsx
index f85fd8423..f85fd8423 100644
--- a/packages/console/app/src/component/workspace/common.tsx
+++ b/packages/console/app/src/routes/workspace/common.tsx
diff --git a/packages/console/app/src/component/workspace/key-section.module.css b/packages/console/app/src/routes/workspace/key-section.module.css
index 6a1d0c85f..6a1d0c85f 100644
--- a/packages/console/app/src/component/workspace/key-section.module.css
+++ b/packages/console/app/src/routes/workspace/key-section.module.css
diff --git a/packages/console/app/src/component/workspace/key-section.tsx b/packages/console/app/src/routes/workspace/key-section.tsx
index a2bd380ea..a2bd380ea 100644
--- a/packages/console/app/src/component/workspace/key-section.tsx
+++ b/packages/console/app/src/routes/workspace/key-section.tsx
diff --git a/packages/console/app/src/routes/workspace/member-section.module.css b/packages/console/app/src/routes/workspace/member-section.module.css
new file mode 100644
index 000000000..16b6ff8d2
--- /dev/null
+++ b/packages/console/app/src/routes/workspace/member-section.module.css
@@ -0,0 +1,179 @@
+.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="members-table"] {
+ overflow-x: auto;
+ }
+
+ [data-slot="members-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="member-email"] {
+ color: var(--color-text);
+ font-family: var(--font-sans);
+ font-weight: 500;
+ }
+
+ &[data-slot="member-role"] {
+ 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="member-date"] {
+ color: var(--color-text);
+ }
+
+ &[data-slot="member-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;
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/packages/console/app/src/routes/workspace/member-section.tsx b/packages/console/app/src/routes/workspace/member-section.tsx
new file mode 100644
index 000000000..602c29efc
--- /dev/null
+++ b/packages/console/app/src/routes/workspace/member-section.tsx
@@ -0,0 +1,189 @@
+import { json, query, action, useParams, createAsync, useSubmission } from "@solidjs/router"
+import { createEffect, createSignal, For, Show } from "solid-js"
+import { withActor } from "~/context/auth.withActor"
+import { createStore } from "solid-js/store"
+import { formatDateUTC, formatDateForTable } from "./common"
+import styles from "./member-section.module.css"
+import { and, Database, eq, sql } from "@opencode/console-core/drizzle/index.js"
+import { UserTable } from "@opencode/console-core/schema/user.sql.js"
+import { Identifier } from "@opencode/console-core/identifier.js"
+
+const removeMember = 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(
+ () =>
+ Database.use((tx) =>
+ tx
+ .update(UserTable)
+ .set({ timeDeleted: sql`now()` })
+ .where(and(eq(UserTable.id, id), eq(UserTable.workspaceID, workspaceID))),
+ ),
+ workspaceID,
+ ),
+ { revalidate: listMembers.key },
+ )
+}, "member.remove")
+
+const inviteMember = 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(
+ () =>
+ Database.use((tx) =>
+ tx
+ .insert(UserTable)
+ .values({
+ id: Identifier.create("user"),
+ name: "",
+ email: name,
+ workspaceID,
+ role: "member",
+ timeJoined: sql`now()`,
+ })
+ .onDuplicateKeyUpdate({ set: { timeJoined: sql`now()` } })
+ .then((data) => ({ error: undefined, data }))
+ .catch((e) => ({ error: e.message as string })),
+ ),
+ workspaceID,
+ ),
+ { revalidate: listMembers.key },
+ )
+}, "member.create")
+
+const listMembers = query(async (workspaceID: string) => {
+ "use server"
+ return withActor(
+ () => Database.use((tx) => tx.select().from(UserTable).where(eq(UserTable.workspaceID, workspaceID))),
+ workspaceID,
+ )
+}, "member.list")
+
+export function MemberCreateForm() {
+ const params = useParams()
+ const submission = useSubmission(inviteMember)
+ 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()}>
+ Invite Member
+ </button>
+ }
+ >
+ <form action={inviteMember} 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 email" />
+ <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 ? "Inviting..." : "Invite"}
+ </button>
+ </div>
+ </form>
+ </Show>
+ )
+}
+
+export function MemberSection() {
+ const params = useParams()
+ const members = createAsync(() => listMembers(params.id))
+
+ return (
+ <section class={styles.root}>
+ <div data-slot="section-title">
+ <h2>Members</h2>
+ <p>Manage your members for accessing opencode services.</p>
+ </div>
+ <MemberCreateForm />
+ <div data-slot="members-table">
+ <Show
+ when={members()?.length}
+ fallback={
+ <div data-component="empty-state">
+ <p>Invite a member to your workspace</p>
+ </div>
+ }
+ >
+ <table data-slot="members-table-element">
+ <thead>
+ <tr>
+ <th>Email</th>
+ <th>Role</th>
+ <th>Joined</th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ <For each={members()!}>
+ {(member) => {
+ const [copied, setCopied] = createSignal(false)
+ // const submission = useSubmission(removeKey, ([fd]) => fd.get("id")?.toString() === key.id)
+ return (
+ <tr>
+ <td data-slot="member-email">{member.email}</td>
+ <td data-slot="member-role">{member.role}</td>
+ <td data-slot="member-joined" title={formatDateUTC(member.timeJoined!)}>
+ {formatDateForTable(member.timeJoined!)}
+ </td>
+ <td data-slot="member-actions">
+ <form action={removeMember} method="post">
+ <input type="hidden" name="id" value={member.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/console/app/src/component/workspace/monthly-limit-section.module.css b/packages/console/app/src/routes/workspace/monthly-limit-section.module.css
index 02de058e4..02de058e4 100644
--- a/packages/console/app/src/component/workspace/monthly-limit-section.module.css
+++ b/packages/console/app/src/routes/workspace/monthly-limit-section.module.css
diff --git a/packages/console/app/src/component/workspace/monthly-limit-section.tsx b/packages/console/app/src/routes/workspace/monthly-limit-section.tsx
index 35da774d0..35da774d0 100644
--- a/packages/console/app/src/component/workspace/monthly-limit-section.tsx
+++ b/packages/console/app/src/routes/workspace/monthly-limit-section.tsx
diff --git a/packages/console/app/src/component/workspace/new-user-section.module.css b/packages/console/app/src/routes/workspace/new-user-section.module.css
index 2edc7cc14..2edc7cc14 100644
--- a/packages/console/app/src/component/workspace/new-user-section.module.css
+++ b/packages/console/app/src/routes/workspace/new-user-section.module.css
diff --git a/packages/console/app/src/component/workspace/new-user-section.tsx b/packages/console/app/src/routes/workspace/new-user-section.tsx
index 5909072dd..5909072dd 100644
--- a/packages/console/app/src/component/workspace/new-user-section.tsx
+++ b/packages/console/app/src/routes/workspace/new-user-section.tsx
diff --git a/packages/console/app/src/component/workspace/payment-section.module.css b/packages/console/app/src/routes/workspace/payment-section.module.css
index 2e1afe78b..2e1afe78b 100644
--- a/packages/console/app/src/component/workspace/payment-section.module.css
+++ b/packages/console/app/src/routes/workspace/payment-section.module.css
diff --git a/packages/console/app/src/component/workspace/payment-section.tsx b/packages/console/app/src/routes/workspace/payment-section.tsx
index 5cdb0cd78..5cdb0cd78 100644
--- a/packages/console/app/src/component/workspace/payment-section.tsx
+++ b/packages/console/app/src/routes/workspace/payment-section.tsx
diff --git a/packages/console/app/src/component/workspace/usage-section.module.css b/packages/console/app/src/routes/workspace/usage-section.module.css
index 1a772ba87..1a772ba87 100644
--- a/packages/console/app/src/component/workspace/usage-section.module.css
+++ b/packages/console/app/src/routes/workspace/usage-section.module.css
diff --git a/packages/console/app/src/component/workspace/usage-section.tsx b/packages/console/app/src/routes/workspace/usage-section.tsx
index e68670c6d..e68670c6d 100644
--- a/packages/console/app/src/component/workspace/usage-section.tsx
+++ b/packages/console/app/src/routes/workspace/usage-section.tsx