summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorFrank <[email protected]>2025-10-06 17:13:15 -0400
committerFrank <[email protected]>2025-10-06 17:13:19 -0400
commitc2f57ea74d752b7590106ea9b5eeca2aac5d3950 (patch)
tree69e06d9b561baa9c78a7b6479e62d1d2773b8079
parent9e8fd16e6e4154ae0bccff8342e4b0c7780d8db8 (diff)
downloadopencode-c2f57ea74d752b7590106ea9b5eeca2aac5d3950.tar.gz
opencode-c2f57ea74d752b7590106ea9b5eeca2aac5d3950.zip
wip: zen
-rw-r--r--packages/console/app/src/routes/workspace/[id].tsx3
-rw-r--r--packages/console/app/src/routes/workspace/settings-section.module.css95
-rw-r--r--packages/console/app/src/routes/workspace/settings-section.tsx124
-rw-r--r--packages/console/core/src/workspace.ts18
4 files changed, 240 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>
+ )
+}
diff --git a/packages/console/core/src/workspace.ts b/packages/console/core/src/workspace.ts
index 36d66e15a..f9591632a 100644
--- a/packages/console/core/src/workspace.ts
+++ b/packages/console/core/src/workspace.ts
@@ -7,6 +7,7 @@ import { UserTable } from "./schema/user.sql"
import { BillingTable } from "./schema/billing.sql"
import { WorkspaceTable } from "./schema/workspace.sql"
import { Key } from "./key"
+import { eq } from "drizzle-orm"
export namespace Workspace {
export const create = fn(
@@ -45,4 +46,21 @@ export namespace Workspace {
return workspaceID
},
)
+
+ export const update = fn(
+ z.object({
+ name: z.string().min(1).max(255),
+ }),
+ async ({ name }) => {
+ const workspaceID = Actor.workspace()
+ return await Database.use((tx) =>
+ tx
+ .update(WorkspaceTable)
+ .set({
+ name,
+ })
+ .where(eq(WorkspaceTable.id, workspaceID)),
+ )
+ },
+ )
}