diff options
| author | Frank <[email protected]> | 2025-10-06 17:13:15 -0400 |
|---|---|---|
| committer | Frank <[email protected]> | 2025-10-06 17:13:19 -0400 |
| commit | c2f57ea74d752b7590106ea9b5eeca2aac5d3950 (patch) | |
| tree | 69e06d9b561baa9c78a7b6479e62d1d2773b8079 /packages/console/app/src | |
| parent | 9e8fd16e6e4154ae0bccff8342e4b0c7780d8db8 (diff) | |
| download | opencode-c2f57ea74d752b7590106ea9b5eeca2aac5d3950.tar.gz opencode-c2f57ea74d752b7590106ea9b5eeca2aac5d3950.zip | |
wip: zen
Diffstat (limited to 'packages/console/app/src')
3 files changed, 222 insertions, 0 deletions
diff --git a/packages/console/app/src/routes/workspace/[id].tsx b/packages/console/app/src/routes/workspace/[id].tsx index 2e59395b9..09b14056a 100644 --- a/packages/console/app/src/routes/workspace/[id].tsx +++ b/packages/console/app/src/routes/workspace/[id].tsx @@ -6,6 +6,7 @@ import { PaymentSection } from "./payment-section" import { UsageSection } from "./usage-section" import { KeySection } from "./key-section" import { MemberSection } from "./member-section" +import { SettingsSection } from "./settings-section" import { Show } from "solid-js" import { createAsync, query, useParams } from "@solidjs/router" import { Actor } from "@opencode-ai/console-core/actor.js" @@ -28,6 +29,7 @@ export default function () { const params = useParams() const userInfo = createAsync(() => getUserInfo(params.id)) const isBeta = createAsync(() => beta(params.id)) + return ( <div data-page="workspace-[id]"> <section data-component="title-section"> @@ -46,6 +48,7 @@ export default function () { <KeySection /> <Show when={userInfo()?.isAdmin}> <Show when={isBeta()}> + <SettingsSection /> <MemberSection /> </Show> <BillingSection /> diff --git a/packages/console/app/src/routes/workspace/settings-section.module.css b/packages/console/app/src/routes/workspace/settings-section.module.css new file mode 100644 index 000000000..e3a5ad508 --- /dev/null +++ b/packages/console/app/src/routes/workspace/settings-section.module.css @@ -0,0 +1,95 @@ +.root { + [data-slot="section-content"] { + display: flex; + flex-direction: column; + gap: var(--space-4); + } + + [data-slot="setting"] { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: var(--space-4); + padding: var(--space-4); + border: 1px solid var(--color-border); + border-radius: var(--border-radius-sm); + + @media (max-width: 30rem) { + flex-direction: column; + gap: var(--space-3); + } + } + + [data-slot="setting-info"] { + flex: 1; + display: flex; + flex-direction: column; + gap: var(--space-1); + + h3 { + font-size: var(--font-size-md); + font-weight: 500; + line-height: 1.2; + margin: 0; + color: var(--color-text); + } + + [data-slot="current-value"] { + font-size: var(--font-size-sm); + color: var(--color-text-muted); + line-height: 1.4; + margin: 0; + } + } + + [data-slot="create-form"] { + display: flex; + flex-direction: column; + gap: var(--space-3); + min-width: 15rem; + width: fit-content; + + @media (max-width: 30rem) { + width: 100%; + min-width: auto; + } + + [data-slot="input-container"] { + display: flex; + flex-direction: column; + gap: var(--space-1); + } + + 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); + justify-content: flex-end; + } + + [data-slot="form-error"] { + color: var(--color-danger); + font-size: var(--font-size-sm); + line-height: 1.4; + } + } +} diff --git a/packages/console/app/src/routes/workspace/settings-section.tsx b/packages/console/app/src/routes/workspace/settings-section.tsx new file mode 100644 index 000000000..0fc0158da --- /dev/null +++ b/packages/console/app/src/routes/workspace/settings-section.tsx @@ -0,0 +1,124 @@ +import { json, action, useParams, useSubmission, createAsync, query } from "@solidjs/router" +import { createEffect, Show } from "solid-js" +import { createStore } from "solid-js/store" +import { withActor } from "~/context/auth.withActor" +import { Workspace } from "@opencode-ai/console-core/workspace.js" +import styles from "./settings-section.module.css" +import { Database, eq } from "@opencode-ai/console-core/drizzle/index.js" +import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js" + +const getWorkspaceInfo = query(async (workspaceID: string) => { + "use server" + return withActor( + () => + Database.use((tx) => + tx + .select({ + id: WorkspaceTable.id, + name: WorkspaceTable.name, + slug: WorkspaceTable.slug, + }) + .from(WorkspaceTable) + .where(eq(WorkspaceTable.id, workspaceID)) + .then((rows) => rows[0] || null), + ), + workspaceID, + ) +}, "workspace.get") + +const updateWorkspace = action(async (form: FormData) => { + "use server" + const name = form.get("name")?.toString().trim() + if (!name) return { error: "Workspace name is required." } + if (name.length > 255) return { error: "Name must be 255 characters or less." } + const workspaceID = form.get("workspaceID")?.toString() + if (!workspaceID) return { error: "Workspace ID is required." } + return json( + await withActor( + () => + Workspace.update({ name }) + .then(() => ({ error: undefined })) + .catch((e) => ({ error: e.message as string })), + workspaceID, + ), + ) +}, "workspace.update") + +export function SettingsSection() { + const params = useParams() + const workspaceInfo = createAsync(() => getWorkspaceInfo(params.id)) + const submission = useSubmission(updateWorkspace) + const [store, setStore] = createStore({ show: false }) + + let input: HTMLInputElement + + createEffect(() => { + if (!submission.pending && submission.result && !submission.result.error) { + hide() + } + }) + + function show() { + while (true) { + submission.clear() + if (!submission.result) break + } + setStore("show", true) + input.focus() + } + + function hide() { + setStore("show", false) + } + + return ( + <section class={styles.root}> + <div data-slot="section-title"> + <h2>Settings</h2> + <p>Update your workspace name and preferences.</p> + </div> + <div data-slot="section-content"> + <div data-slot="setting"> + <div data-slot="setting-info"> + <h3>Workspace Name</h3> + <p data-slot="current-value">{workspaceInfo()?.name}</p> + </div> + <Show + when={!store.show} + fallback={ + <form action={updateWorkspace} method="post" data-slot="create-form"> + <div data-slot="input-container"> + <input + required + ref={(r) => (input = r)} + data-component="input" + name="name" + type="text" + placeholder="Workspace name" + value={workspaceInfo()?.name ?? "Default"} + /> + <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 ? "Updating..." : "Update"} + </button> + </div> + </form> + } + > + <button data-color="primary" onClick={() => show()}> + Edit Name + </button> + </Show> + </div> + </div> + </section> + ) +} |
