mirror of
https://github.com/aljazceru/opencode.git
synced 2025-12-21 09:44:21 +01:00
wip: zen
This commit is contained in:
@@ -6,6 +6,7 @@ import { PaymentSection } from "./payment-section"
|
|||||||
import { UsageSection } from "./usage-section"
|
import { UsageSection } from "./usage-section"
|
||||||
import { KeySection } from "./key-section"
|
import { KeySection } from "./key-section"
|
||||||
import { MemberSection } from "./member-section"
|
import { MemberSection } from "./member-section"
|
||||||
|
import { SettingsSection } from "./settings-section"
|
||||||
import { Show } from "solid-js"
|
import { Show } from "solid-js"
|
||||||
import { createAsync, query, useParams } from "@solidjs/router"
|
import { createAsync, query, useParams } from "@solidjs/router"
|
||||||
import { Actor } from "@opencode-ai/console-core/actor.js"
|
import { Actor } from "@opencode-ai/console-core/actor.js"
|
||||||
@@ -28,6 +29,7 @@ export default function () {
|
|||||||
const params = useParams()
|
const params = useParams()
|
||||||
const userInfo = createAsync(() => getUserInfo(params.id))
|
const userInfo = createAsync(() => getUserInfo(params.id))
|
||||||
const isBeta = createAsync(() => beta(params.id))
|
const isBeta = createAsync(() => beta(params.id))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-page="workspace-[id]">
|
<div data-page="workspace-[id]">
|
||||||
<section data-component="title-section">
|
<section data-component="title-section">
|
||||||
@@ -46,6 +48,7 @@ export default function () {
|
|||||||
<KeySection />
|
<KeySection />
|
||||||
<Show when={userInfo()?.isAdmin}>
|
<Show when={userInfo()?.isAdmin}>
|
||||||
<Show when={isBeta()}>
|
<Show when={isBeta()}>
|
||||||
|
<SettingsSection />
|
||||||
<MemberSection />
|
<MemberSection />
|
||||||
</Show>
|
</Show>
|
||||||
<BillingSection />
|
<BillingSection />
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
124
packages/console/app/src/routes/workspace/settings-section.tsx
Normal file
124
packages/console/app/src/routes/workspace/settings-section.tsx
Normal file
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import { UserTable } from "./schema/user.sql"
|
|||||||
import { BillingTable } from "./schema/billing.sql"
|
import { BillingTable } from "./schema/billing.sql"
|
||||||
import { WorkspaceTable } from "./schema/workspace.sql"
|
import { WorkspaceTable } from "./schema/workspace.sql"
|
||||||
import { Key } from "./key"
|
import { Key } from "./key"
|
||||||
|
import { eq } from "drizzle-orm"
|
||||||
|
|
||||||
export namespace Workspace {
|
export namespace Workspace {
|
||||||
export const create = fn(
|
export const create = fn(
|
||||||
@@ -45,4 +46,21 @@ export namespace Workspace {
|
|||||||
return workspaceID
|
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)),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user