-
+
{
+ if (url.params.dep) {
+ getDeployment.fetch({ url: url.params.dep })
+ }
+})
+
+const navItems = [
+ { id: 'project', label: 'Project', icon: Settings },
+ { id: 'deployments', label: 'Deployments', icon: Cloud },
+ { id: 'team', label: 'Team Members', icon: Users },
+]
+const formatDate = (d?: number | null) =>
+ d ? new Date(d).toLocaleDateString() : undefined
+const isEditing = (key: string) => url.params.editing === key
+
+const rowClass =
+ 'flex justify-between items-center py-2 border-b border-base-300 last:border-0'
+const inputXs =
+ 'input input-xs input-bordered w-full text-[11px] bg-base-100/50 h-7'
+const btnXs = 'btn btn-xs btn-square join-item h-7 min-h-0'
+
+const SettingsSidebar = () => (
+
+)
+
+const Layout = ({
+ title,
+ desc,
+ actions,
+ error,
+ children,
+}: {
+ title: string
+ desc: string
+ actions?: preact.ComponentChildren
+ error?: string | null
+ children: preact.ComponentChildren
+}) => (
+
+
+
+
+ {error && (
+
+
+ {error}
+
+ )}
+ {children}
+
+
+
+)
+
+const Card = (
+ { title, action, children }: {
+ title: string
+ action?: preact.ComponentChildren
+ children: preact.ComponentChildren
+ },
+) => (
+
+
+
{title}
+ {action}
+
+
{children}
+
+)
+
+const EditCard = (
+ { title, editKey, saving, children }: {
+ title: string
+ editKey: string
+ saving?: boolean
+ children: preact.ComponentChildren
+ },
+) => (
+
+
+
{title}
+ {isEditing(editKey)
+ ? (
+
+
+
+
+
+
+ )
+ : (
+
+ Edit
+
+ )}
+
+
{children}
+
+)
+
+const Row = (
+ { label, value }: { label: string; value?: string | number | null },
+) => (
+
+ {label}
+ {value ?? '–'}
+
+)
+
+const TextRow = (
+ { label, name, value, editKey }: {
+ label: string
+ name: string
+ value: string
+ editKey: string
+ },
+) => (
+
+ {label}
+ {isEditing(editKey)
+ ? (
+
+ )
+ : {value || '–'}}
+
+)
+
+const ToggleRow = (
+ { label, name, value, editKey }: {
+ label: string
+ name?: string
+ value: boolean
+ editKey: string
+ },
+) => (
+
+ {label}
+ {isEditing(editKey)
+ ? (
+
+ )
+ : {value ? 'Yes' : 'No'}}
+
+)
+
+const Label = ({ text, desc }: { text: string; desc: string }) => (
+
+ {text}
+ • {desc}
+
+)
+
+const Input = (
+ { name, value, editKey, placeholder, secret, visKey, mono }: {
+ name: string
+ value: string
+ editKey: string
+ placeholder?: string
+ secret?: boolean
+ visKey?: string
+ mono?: boolean
+ },
+) => {
+ const visible = visKey ? url.params[visKey] === 'true' : !secret
+ return (
+
+ )
+}
+
+const Accordion = ({
+ label,
+ name,
+ value,
+ editKey,
+ urlKey,
+ hideOnEdit,
+ children,
+}: {
+ label: string
+ name: string
+ value: boolean
+ editKey: string
+ urlKey: string
+ hideOnEdit?: boolean
+ children: preact.ComponentChildren
+}) => {
+ const param = url.params[urlKey]
+ const enabled = param != null ? param === 'true' : value
+ const editing = isEditing(editKey)
+ const showChildren = enabled && !(hideOnEdit && editing)
+ const onChange = (v: boolean) =>
+ navigate({ params: { [urlKey]: String(v) }, replace: true })
+ return (
+
+
+ {label}
+ {editing
+ ? (
+ onChange(e.currentTarget.checked)}
+ class='toggle toggle-sm toggle-primary'
+ />
+ )
+ : {enabled ? 'Yes' : 'No'}}
+
+ {showChildren && (
+
+ {children}
+
+ )}
+
+ )
+}
+
+const ToolCard = (
+ { title, desc, empty }: { title: string; desc: string; empty: string },
+) => (
+
+)
+
+const addDeploymentError = signal(null)
+const handleSubmit = async (e: TargetedEvent) => {
+ e.preventDefault()
+ const fd = new FormData(e.currentTarget)
+ const projectId = project.data?.slug
+ if (!projectId) return
+ try {
+ const depUrl = new URL(fd.get('url') as string).host
+ await createDeployment.fetch({
+ url: depUrl,
+ projectId,
+ logsEnabled: false,
+ databaseEnabled: false,
+ sqlEndpoint: null,
+ sqlToken: null,
+ })
+ deployments.fetch({ project: projectId })
+ navigate({
+ params: { dialog: null },
+ replace: true,
+ })
+ } catch (e) {
+ addDeploymentError.value = e instanceof Error ? e.message : 'Unknown error'
+ }
+}
+const AddDeploymentDialog = () => (
+
+ Add Deployment
+
+
+)
+
+function LogsTokenSection({ deploymentUrl }: { deploymentUrl: string }) {
+ const visible = url.params['token-vis'] === 'true'
+ const loading = getDeployment.pending || regenToken.pending
+
+ const regenerate = async () => {
+ if (!confirm('Regenerate token?')) return
+ await regenToken.fetch({ url: deploymentUrl })
+ getDeployment.fetch({ url: deploymentUrl })
+ }
+
+ return (
+
+
+
+ {(getDeployment.error || regenToken.error) && (
+
+ {(getDeployment.error || regenToken.error)?.message}
+
+ )}
+
+ )
+}
+
+// Pages
+const handleProjectSubmit = async (e: TargetedEvent) => {
+ e.preventDefault()
+ if (!project.data) return
+ const fd = new FormData(e.currentTarget)
+ try {
+ await updateProject.fetch({
+ ...project.data,
+ name: fd.get('name') as string,
+ repositoryUrl: (fd.get('repositoryUrl') as string) || undefined,
+ isPublic: fd.get('isPublic') === 'on',
+ })
+ project.fetch({ slug: project.data.slug })
+ navigate({ params: { editing: null }, replace: true })
+ } catch (e) {
+ console.error(e)
+ }
+}
+const ProjectSettingsPage = () => {
+ const p = project.data, deps = deployments.data ?? []
+ return (
+
+
+
+
+
+
+ Add
+
+ }
+ >
+ {deps.length === 0
+ ? (
+
+ No deployments configured.
+
+ )
+ : (
+
+ )}
+
+
+ )
+}
+
+const handleDeploymentSubmit = async (e: TargetedEvent) => {
+ e.preventDefault()
+ const dep = getDeployment.data
+ if (!dep) return
+ const { logs, db } = url.params
+ const logsEnabled = logs != null
+ ? logs === 'true'
+ : (dep?.logsEnabled ?? false)
+ const databaseEnabled = db != null
+ ? db === 'true'
+ : (dep?.databaseEnabled ?? false)
+ const fd = new FormData(e.currentTarget)
+ try {
+ await updateDeployment.fetch({
+ url: dep.url,
+ projectId: project.data!.slug,
+ logsEnabled,
+ databaseEnabled,
+ sqlEndpoint: (fd.get('sql-endpoint') as string) || undefined,
+ sqlToken: (fd.get('sql-token') as string) || undefined,
+ })
+ getDeployment.fetch({ url: dep.url })
+ navigate({
+ params: { editing: null, logs: null, db: null },
+ replace: true,
+ })
+ } catch (e) {
+ console.error(e)
+ }
+}
+const DeploymentsSettingsPage = () => {
+ const deps = deployments.data ?? [],
+ dep = getDeployment.data,
+ selectedUrl = url.params.dep
+
+ if (!selectedUrl && deps.length > 0) {
+ navigate({ params: { dep: deps[0].url }, replace: true })
+ return null
+ }
+
+ return (
+
+
+ New
+
+ {deps.length > 0 && (
+
+ )}
+
+ }
+ >
+ {dep && (
+
+ )}
+ {dep && (
+
+
+
+
+
+
+ )}
+ {!dep && deps.length > 0 && (
+
+ Select a deployment to view its configuration.
+
+ )}
+ {getDeployment.pending && (
+
+ Loading deployment...
+
+ )}
+
+ )
+}
+
+const TeamSettingsPage = () => (
+
+
+ Team settings content will go here.
+
+
+)
+
+const views = {
+ project: ProjectSettingsPage,
+ deployments: DeploymentsSettingsPage,
+ team: TeamSettingsPage,
+} as const
+
+export const SettingsPage = () => {
+ const { view = 'project' } = url.params
+ if (!project.data) {
+ return (
+
+
+
+ )
+ }
+ const Content = views[view as keyof typeof views] ?? views.project
+ return (
+
+
+
+
+
+ {project.data &&
}
+
+ )
+}
diff --git a/web/pages/project/SettingsPage.tsx b/web/pages/project/SettingsPage.tsx
deleted file mode 100644
index 6c414a4..0000000
--- a/web/pages/project/SettingsPage.tsx
+++ /dev/null
@@ -1,297 +0,0 @@
-import { useSignal } from '@preact/signals'
-import { navigate, url } from '@01edu/signal-router'
-import type { TargetedEvent } from 'preact'
-import { PageContent, PageHeader } from '../../components/Layout.tsx'
-import { Button, Card, Input, Switch } from '../../components/forms.tsx'
-import { api, ApiOutput } from '../../lib/api.ts'
-import { user } from '../../lib/session.ts'
-import { deployments, project } from '../../lib/shared.tsx'
-
-type Project = ApiOutput['GET/api/projects'][number]
-type Deployment = ApiOutput['GET/api/project/deployments'][number]
-type User = ApiOutput['GET/api/users'][number]
-
-const users = api['GET/api/users'].signal()
-users.fetch()
-
-const team = api['GET/api/team'].signal()
-
-function ProjectInfoForm() {
- const handleSubmit = (e: TargetedEvent
) => {
- e.preventDefault()
- }
-
- return (
-
-
-
- )
-}
-
-function DeploymentForm({ deployment }: { deployment?: Deployment }) {
- const databaseEnabled = useSignal(deployment?.databaseEnabled || false)
- const handleSubmit = (e: TargetedEvent) => {
- e.preventDefault()
- }
-
- return (
-
-
-
- )
-}
-
-function DeploymentsList({ deployments }: { deployments: Deployment[] }) {
- const handleDelete = (_id: string) => {
- }
-
- return (
-
-
- {deployments.map((dep) => (
-
-
-
{dep.url}
-
- Logs: {dep.logsEnabled ? 'Enabled' : 'Disabled'} | Database:
- {' '}
- {dep.databaseEnabled ? 'Enabled' : 'Disabled'}
-
-
-
-
-
-
-
- ))}
-
-
-
-
-
- )
-}
-
-function UserManagement() {
- const teamMembersDetails = team.data?.teamMembers.map((email) =>
- users.data?.find((u) => u.userEmail === email)
- ).filter(Boolean) as User[]
-
- const handleAddUser = (e: TargetedEvent) => {
- e.preventDefault()
- }
-
- const handleRemoveUser = (_email: string) => {
- }
-
- return (
-
-
- {teamMembersDetails.map((member) => (
-
-
-

-
-
{member.userFullName}
-
- {member.userEmail}
-
-
-
-
-
- ))}
-
-
-
- )
-}
-
-export const SettingsPage = () => {
- if (!user.data?.isAdmin) {
- navigate({ params: { nav: 'deployments' } })
- }
- const { view = 'info', action, id } = url.params
- if (!project.data) return null
-
- team.fetch({ teamId: project.data!.teamId })
-
- const content = view === 'deployments'
- ? (
- action === 'add'
- ?
- : action === 'edit' && id
- ? (
- d.url === id)}
- />
- )
- :
- )
- : view === 'users'
- ?
- :
-
- return (
- <>
-
-
- Project Settings: {project.data?.name}
-
-
-
-
-
-
-
-
-
- {content}
-
-
- >
- )
-}