diff options
| author | Frank <[email protected]> | 2025-09-28 19:55:38 -0400 |
|---|---|---|
| committer | Frank <[email protected]> | 2025-09-28 19:55:40 -0400 |
| commit | 918739057d28ec440547bbe1299c1b6da9cf5485 (patch) | |
| tree | f29060dc58ab35ac9a5ca193f99e601f5c8a357b /packages/console/app/src | |
| parent | e3a7096e447d3e9b217f870da4623fab34298479 (diff) | |
| download | opencode-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].tsx | 21 | ||||
| -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.css | 179 | ||||
| -rw-r--r-- | packages/console/app/src/routes/workspace/member-section.tsx | 189 | ||||
| -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 |
