From 573979661963458a95d36157e96c89ee63f23490 Mon Sep 17 00:00:00 2001 From: Abdou TOP Date: Fri, 30 Jan 2026 13:55:29 +0000 Subject: [PATCH 1/3] refactor: reimplement SettingsPage and adjust API routes. --- api/routes.ts | 4 +- web/pages/ProjectPage.tsx | 4 +- web/pages/SettingsPage.tsx | 177 +++++++++++++++++ web/pages/project/SettingsPage.tsx | 297 ----------------------------- 4 files changed, 181 insertions(+), 301 deletions(-) create mode 100644 web/pages/SettingsPage.tsx delete mode 100644 web/pages/project/SettingsPage.tsx diff --git a/api/routes.ts b/api/routes.ts index 7d5a5ef..96fd4ad 100644 --- a/api/routes.ts +++ b/api/routes.ts @@ -91,7 +91,7 @@ const defs = { authorize: withUserSession, fn: ({ session }) => session, output: UserDef, - description: 'Handle Google OAuth callback', + description: 'Get current authenticated user information', }), 'GET/api/picture': route({ fn: (_ctx, { hash }) => getPicture(hash), @@ -316,7 +316,7 @@ const defs = { output: deploymentOutput, description: 'Update a deployment by ID', }), - 'GET/api/deployment/token/regenerate': route({ + 'POST/api/deployment/token/regenerate': route({ authorize: withAdminSession, fn: async (_ctx, { url }) => { const dep = DeploymentsCollection.get(url) diff --git a/web/pages/ProjectPage.tsx b/web/pages/ProjectPage.tsx index 95749d8..08bea21 100644 --- a/web/pages/ProjectPage.tsx +++ b/web/pages/ProjectPage.tsx @@ -2,7 +2,7 @@ import { effect } from '@preact/signals' import { navigate, url } from '@01edu/signal-router' import { Sidebar } from '../components/SideBar.tsx' import { user } from '../lib/session.ts' -import { SettingsPage } from './project/SettingsPage.tsx' +import { SettingsPage } from './SettingsPage.tsx' import { deployments, project, sidebarItems } from '../lib/shared.tsx' effect(() => { @@ -31,7 +31,7 @@ export function ProjectPage() { return (
-
+
( +
+

+ Settings +

+

+ {project.data?.name} +

+
+) + +const SidebarNavItem = ( + { item, isActive }: { item: NavItem; isActive: boolean }, +) => ( +
  • + + + {item.label} + + +
  • +) + +const SidebarNav = () => ( + +) + +const SidebarFooter = () => ( +
    +
    +
    + Connected +
    +
    +) + +const SettingsSidebar = () => ( + +) + +const PageHeader = ( + { title, description }: { title: string; description: string }, +) => ( +
    +

    {title}

    +

    {description}

    +
    +) + +const ProjectSettingsPage = () => ( +
    + +
    +
    +
    + Project settings content will go here. +
    +
    +
    +
    +) + +const DeploymentsSettingsPage = () => ( +
    + +
    +
    +
    + Deployment settings content will go here. +
    +
    +
    +
    +) + +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 ( +
    + +
    + +
    +
    + ) +} 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 ( - -
    -
    - - - databaseEnabled.value = e.currentTarget.checked} - note='Provide an endpoint to execute SQL queries against your database.' - /> - {databaseEnabled.value && ( -
    - - -
    - )} -
    -
    - - -
    -
    -
    - ) -} - -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.userFullName}

    -

    - {member.userEmail} -

    -
    -
    - -
    - ))} -
    -
    -

    Add a new user

    -
    - -
    - -
    -
    -
    -
    - ) -} - -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} -
    -
    - - ) -} From 03f554ea54290fdfcfe021bd1ea4837cac22fa91 Mon Sep 17 00:00:00 2001 From: Abdou TOP Date: Sat, 31 Jan 2026 08:33:20 +0000 Subject: [PATCH 2/3] feat: Implement detailed project and deployment settings pages with deployment management and token regeneration, update `GET/api/deployment` input, and refine UI layout. --- api/routes.ts | 4 +- web/index.tsx | 2 +- web/pages/ProjectPage.tsx | 4 +- web/pages/SettingsPage.tsx | 544 ++++++++++++++++++++++++++++++++++--- 4 files changed, 516 insertions(+), 38 deletions(-) diff --git a/api/routes.ts b/api/routes.ts index 96fd4ad..407e71a 100644 --- a/api/routes.ts +++ b/api/routes.ts @@ -262,7 +262,7 @@ const defs = { }), 'GET/api/deployment': route({ authorize: withAdminSession, - fn: async (_ctx, url) => { + fn: async (_ctx, { url }) => { const dep = DeploymentsCollection.get(url) if (!dep) throw respond.NotFound() const { tokenSalt, ...deployment } = dep @@ -274,7 +274,7 @@ const defs = { token, } }, - input: STR(), + input: OBJ({ url: STR('Deployment URL') }), output: deploymentOutput, description: 'Get a deployment by ID', }), diff --git a/web/index.tsx b/web/index.tsx index c070327..eee60ee 100644 --- a/web/index.tsx +++ b/web/index.tsx @@ -26,7 +26,7 @@ const App = () => {
    -
    +
    {renderPage()}
    diff --git a/web/pages/ProjectPage.tsx b/web/pages/ProjectPage.tsx index 08bea21..d437846 100644 --- a/web/pages/ProjectPage.tsx +++ b/web/pages/ProjectPage.tsx @@ -29,9 +29,9 @@ export function ProjectPage() { } return ( -
    +
    -
    +
    ( ) const PageHeader = ( - { title, description }: { title: string; description: string }, + { title, description, children }: { + title: string + description: string + children?: preact.ComponentChildren + }, ) => ( -
    -

    {title}

    -

    {description}

    +
    +
    +

    {title}

    +

    {description}

    +
    + {children}
    ) -const ProjectSettingsPage = () => ( -
    - -
    -
    -
    - Project settings content will go here. -
    -
    +const InfoRow = ({ label, value }: { label: string; value: string | number | undefined | null }) => ( +
    + {label} + {value ?? '–'} +
    +) + +const Card = ({ title, action, children }: { title: string; action?: preact.ComponentChildren; children: preact.ComponentChildren }) => ( +
    +
    +

    {title}

    + {action}
    +
    {children}
    ) -const DeploymentsSettingsPage = () => ( -
    - -
    -
    -
    - Deployment settings content will go here. +const EditableRow = ( + { label, name, value, editing }: { label: string; name: string; value: string; editing: boolean }, +) => ( +
    + {label} + {editing ? ( + + ) : ( + {value || '–'} + )} +
    +) + +function AddDeploymentDialog({ projectId }: { projectId: string }) { + const { dialog } = url.params + if (dialog !== 'add-deployment') return null + + const handleSubmit = async (e: TargetedEvent) => { + e.preventDefault() + const formData = new FormData(e.currentTarget) + let deploymentUrl = formData.get('url') as string + + // Sanitize input to get domain only + try { + const url = new URL(deploymentUrl.match(/^https?:\/\//) ? deploymentUrl : `https://${deploymentUrl}`) + deploymentUrl = url.host + } catch { + deploymentUrl = deploymentUrl.replace(/^https?:\/\//, '').split('/')[0] + } + + try { + await api['POST/api/deployment'].fetch({ + url: deploymentUrl, + projectId: projectId, + logsEnabled: false, + databaseEnabled: false, + sqlEndpoint: null, + sqlToken:null + }) + navigate({ params: { dialog: null, view: 'deployments', url: deploymentUrl }, replace: true }) + deployments.fetch({ project: projectId }) + } catch (err) { + console.error(err) + // Ideally show toast here + } + } + + return ( + +
    +

    Add Deployment

    +
    +
    +
    + + +
    + +
    +
    + ) +} + +function LogsTokenSection({ deploymentUrl }: { deploymentUrl: string }) { + const [token, setToken] = useState(null) + const [loading, setLoading] = useState(false) + const [visible, setVisible] = useState(false) + + const fetchToken = async () => { + setLoading(true) + try { + const dep = await api['GET/api/deployment'].fetch({ url: deploymentUrl }) + if (dep.token) setToken(dep.token) + } catch (err) { + console.error(err) + } finally { + setLoading(false) + } + } + + const regenerateToken = async () => { + if (!confirm('Are you sure? This will invalidate the existing token.')) return + setLoading(true) + try { + const dep = await api['POST/api/deployment/token/regenerate'].fetch({ url: deploymentUrl }) + if (dep.token) setToken(dep.token) + } catch (err) { + console.error(err) + } finally { + setLoading(false) + } + } + + const toggleVisibility = () => { + if (!visible && !token) { + fetchToken() + } + setVisible(!visible) + } + + return ( +
    + +
    + + +
    -
    -) + ) +} + +const ProjectSettingsPage = () => { + const p = project.data + const deps = deployments.data ?? [] + const { editing, saving } = url.params + const isEditing = editing === 'project' + const isSaving = saving === 'true' + + const handleSubmit = async (e: TargetedEvent) => { + e.preventDefault() + if (!p) return + + navigate({ params: { saving: 'true' }, replace: true }) + const formData = new FormData(e.currentTarget) + const name = formData.get('name') as string + const repositoryUrl = formData.get('repositoryUrl') as string + const isPublic = formData.get('isPublic') === 'on' + + try { + await api['PUT/api/project'].fetch({ + ...p, + name, + repositoryUrl: repositoryUrl || undefined, + isPublic, + }) + project.fetch({ slug: p.slug }) + navigate({ params: { editing: null, saving: null }, replace: true }) + } catch (err) { + console.error(err) + navigate({ params: { saving: null }, replace: true }) + } + } + + return ( +
    + +
    +
    +
    +
    +
    +

    Project Information

    + {isEditing ? ( +
    + + + + +
    + ) : ( + + Edit + + )} +
    +
    + + + +
    + Visibility + {isEditing ? ( + + ) : ( + {p?.isPublic ? 'Public' : 'Private'} + )} +
    + + +
    +
    +
    + + + + + + + Add + + } + > + {deps.length === 0 ? ( +

    No deployments configured.

    + ) : ( + + )} +
    +
    +
    +
    + ) +} + +const DeploymentsSettingsPage = () => { + const deps = deployments.data ?? [] + const { url: selectedUrl, editing, saving } = url.params + const selectedDep = deps.find((d) => d.url === selectedUrl) + const isEditing = editing === 'deployment' + const isSaving = saving === 'true' + + const handleSubmit = async (e: TargetedEvent) => { + e.preventDefault() + if (!selectedDep || !project.data) return + + navigate({ params: { saving: 'true' }, replace: true }) + const formData = new FormData(e.currentTarget) + const logsEnabled = formData.get('logsEnabled') === 'on' + const databaseEnabled = formData.get('databaseEnabled') === 'on' + const sqlEndpoint = formData.get('sqlEndpoint') as string + const sqlToken = formData.get('sqlToken') as string + + try { + await api['PUT/api/deployment'].fetch({ + url: selectedDep.url, + projectId: project.data.slug, + logsEnabled, + databaseEnabled, + sqlEndpoint: sqlEndpoint || undefined, + sqlToken: sqlToken || undefined, + }) + deployments.fetch({ project: project.data.slug }) + navigate({ params: { editing: null, saving: null }, replace: true }) + } catch (err) { + console.error(err) + navigate({ params: { saving: null }, replace: true }) + } + } + + return ( +
    + +
    + + New + + {deps.length > 0 && ( + + )} +
    +
    +
    +
    + {selectedDep && ( +
    +
    +
    +

    Deployment Configuration

    + {isEditing ? ( +
    + + + + +
    + ) : ( + + Edit + + )} +
    +
    + + +
    + Logs Enabled + {isEditing ? ( + + ) : ( + {selectedDep.logsEnabled ? 'Yes' : 'No'} + )} +
    + +
    + Database Enabled + {isEditing ? ( + + ) : ( + {selectedDep.databaseEnabled ? 'Yes' : 'No'} + )} +
    + + {isEditing && ( + <> +
    + SQL Endpoint + +
    +
    + SQL Token + +
    + + )} + + {selectedDep.logsEnabled && ( + + )} + + + +
    +
    +
    + )} + + {/* Tools Section */} + {selectedDep && ( + +
    +
    +

    Column Encryptors

    +

    + Add JS encryptors to encrypt specific columns in your database tables. +

    +
    + No encryptors configured. Click to add one. +
    + +
    + +
    +

    Data Transformers

    +

    + Transform data before displaying or exporting. +

    +
    + No transformers configured. +
    + +
    +
    +
    + )} + + {!selectedDep && deps.length > 0 && ( +
    + Select a deployment to view its configuration. +
    + )} +
    +
    +
    + ) +} const TeamSettingsPage = () => (
    @@ -169,9 +646,10 @@ export const SettingsPage = () => { return (
    -
    +
    -
    +
    + {project.data && }
    ) } From 831567b9c3c16b33c7e9cc2cb85c33eecfe53076 Mon Sep 17 00:00:00 2001 From: Abdou TOP Date: Mon, 2 Feb 2026 23:18:50 +0000 Subject: [PATCH 3/3] refactor: Migrate settings and deployment pages to `@preact/signals` and introduce new reusable UI components. --- web/pages/DeploymentPage.tsx | 4 +- web/pages/SettingsPage.tsx | 1177 +++++++++++++++++++--------------- 2 files changed, 646 insertions(+), 535 deletions(-) diff --git a/web/pages/DeploymentPage.tsx b/web/pages/DeploymentPage.tsx index b4f1b25..ab5c976 100644 --- a/web/pages/DeploymentPage.tsx +++ b/web/pages/DeploymentPage.tsx @@ -841,8 +841,8 @@ function TabNavigation({ const logData = api['POST/api/deployment/logs'].signal() effect(() => { - const { dep, lq } = url.params - if (dep) { + const { dep, lq, sbi } = url.params + if (dep && sbi === 'deployment') { const filterRows = parseFilters('l').filter((r) => r.key !== 'key' && r.value ).map((r) => ({ diff --git a/web/pages/SettingsPage.tsx b/web/pages/SettingsPage.tsx index 5817d01..2f6a08d 100644 --- a/web/pages/SettingsPage.tsx +++ b/web/pages/SettingsPage.tsx @@ -1,629 +1,741 @@ -import { useState } from 'preact/hooks' import { A, navigate, url } from '@01edu/signal-router' + import { api } from '../lib/api.ts' import { deployments, project } from '../lib/shared.tsx' -import { Check, ChevronRight, Cloud, Eye, EyeOff, Loader2, Pencil, Plus, RefreshCw, Settings, Users, X } from 'lucide-preact' +import { + Check, + ChevronRight, + Cloud, + Eye, + EyeOff, + Loader2, + Pencil, + Plus, + RefreshCw, + Settings, + Users, + X, +} from 'lucide-preact' import { DialogModal } from '../components/Dialog.tsx' import type { TargetedEvent } from 'preact' - -// 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 teams = api['GET/api/teams'].signal() -// teams.fetch() - -// const team = api['GET/api/team'].signal() +import { effect, signal } from '@preact/signals' + +// API Signals +const updateProject = api['PUT/api/project'].signal() +const updateDeployment = api['PUT/api/deployment'].signal() +const createDeployment = api['POST/api/deployment'].signal() +const getDeployment = api['GET/api/deployment'].signal() +const regenToken = api['POST/api/deployment/token/regenerate'].signal() + +effect(() => { + 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 -type NavItem = (typeof navItems)[number] +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 SidebarHeader = () => ( -
    -

    - Settings -

    -

    - {project.data?.name} -

    -
    -) - -const SidebarNavItem = ( - { item, isActive }: { item: NavItem; isActive: boolean }, -) => ( -
  • - - - {item.label} - - -
  • -) - -const SidebarNav = () => ( - +const SettingsSidebar = () => ( + ) -const SidebarFooter = () => ( -
    -
    -
    - Connected +const Layout = ({ + title, + desc, + actions, + error, + children, +}: { + title: string + desc: string + actions?: preact.ComponentChildren + error?: string | null + children: preact.ComponentChildren +}) => ( +
    +
    +
    +

    {title}

    +

    {desc}

    +
    + {actions} +
    +
    +
    + {error && ( +
    + + {error} +
    + )} + {children} +
    ) -const SettingsSidebar = () => ( - +const Card = ( + { title, action, children }: { + title: string + action?: preact.ComponentChildren + children: preact.ComponentChildren + }, +) => ( +
    +
    +

    {title}

    + {action} +
    +
    {children}
    +
    ) -const PageHeader = ( - { title, description, children }: { +const EditCard = ( + { title, editKey, saving, children }: { title: string - description: string - children?: preact.ComponentChildren + editKey: string + saving?: boolean + children: preact.ComponentChildren }, ) => ( -
    -
    -

    {title}

    -

    {description}

    +
    +
    +

    {title}

    + {isEditing(editKey) + ? ( +
    + + + + +
    + ) + : ( + + Edit + + )}
    - {children} +
    {children}
    ) -const InfoRow = ({ label, value }: { label: string; value: string | number | undefined | null }) => ( -
    +const Row = ( + { label, value }: { label: string; value?: string | number | null }, +) => ( +
    {label} {value ?? '–'}
    ) -const Card = ({ title, action, children }: { title: string; action?: preact.ComponentChildren; children: preact.ComponentChildren }) => ( -
    -
    -

    {title}

    - {action} -
    -
    {children}
    +const TextRow = ( + { label, name, value, editKey }: { + label: string + name: string + value: string + editKey: string + }, +) => ( +
    + {label} + {isEditing(editKey) + ? ( + + ) + : {value || '–'}}
    ) -const EditableRow = ( - { label, name, value, editing }: { label: string; name: string; value: string; editing: boolean }, +const ToggleRow = ( + { label, name, value, editKey }: { + label: string + name?: string + value: boolean + editKey: string + }, ) => ( -
    +
    {label} - {editing ? ( - - ) : ( - {value || '–'} - )} + {isEditing(editKey) + ? ( + + ) + : {value ? 'Yes' : 'No'}}
    ) -function AddDeploymentDialog({ projectId }: { projectId: string }) { - const { dialog } = url.params - if (dialog !== 'add-deployment') return null +const Label = ({ text, desc }: { text: string; desc: string }) => ( +
    + {text} + • {desc} +
    +) - const handleSubmit = async (e: TargetedEvent) => { - e.preventDefault() - const formData = new FormData(e.currentTarget) - let deploymentUrl = formData.get('url') as string - - // Sanitize input to get domain only - try { - const url = new URL(deploymentUrl.match(/^https?:\/\//) ? deploymentUrl : `https://${deploymentUrl}`) - deploymentUrl = url.host - } catch { - deploymentUrl = deploymentUrl.replace(/^https?:\/\//, '').split('/')[0] - } - - try { - await api['POST/api/deployment'].fetch({ - url: deploymentUrl, - projectId: projectId, - logsEnabled: false, - databaseEnabled: false, - sqlEndpoint: null, - sqlToken:null - }) - navigate({ params: { dialog: null, view: 'deployments', url: deploymentUrl }, replace: true }) - deployments.fetch({ project: projectId }) - } catch (err) { - console.error(err) - // Ideally show toast here - } - } +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 ( +
    + + {secret && visKey && ( + + {visible ? : } + + )} +
    + ) +} +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 ( - -
    -

    Add Deployment

    +
    +
    + {label} + {editing + ? ( + onChange(e.currentTarget.checked)} + class='toggle toggle-sm toggle-primary' + /> + ) + : {enabled ? 'Yes' : 'No'}}
    -
    -
    - - + {showChildren && ( +
    + {children}
    - - - + )} +
    ) } -function LogsTokenSection({ deploymentUrl }: { deploymentUrl: string }) { - const [token, setToken] = useState(null) - const [loading, setLoading] = useState(false) - const [visible, setVisible] = useState(false) +const ToolCard = ( + { title, desc, empty }: { title: string; desc: string; empty: string }, +) => ( +
    +

    {title}

    +

    {desc}

    +
    {empty}
    + + Add {title.split(' ').pop()} + +
    +) - const fetchToken = async () => { - setLoading(true) - try { - const dep = await api['GET/api/deployment'].fetch({ url: deploymentUrl }) - if (dep.token) setToken(dep.token) - } catch (err) { - console.error(err) - } finally { - setLoading(false) - } +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

    +
    +
    + + +
    + {(createDeployment.error || addDeploymentError.value) && ( +
    + {createDeployment.error || addDeploymentError.value} +
    + )} + +
    +
    +) - const regenerateToken = async () => { - if (!confirm('Are you sure? This will invalidate the existing token.')) return - setLoading(true) - try { - const dep = await api['POST/api/deployment/token/regenerate'].fetch({ url: deploymentUrl }) - if (dep.token) setToken(dep.token) - } catch (err) { - console.error(err) - } finally { - setLoading(false) - } - } +function LogsTokenSection({ deploymentUrl }: { deploymentUrl: string }) { + const visible = url.params['token-vis'] === 'true' + const loading = getDeployment.pending || regenToken.pending - const toggleVisibility = () => { - if (!visible && !token) { - fetchToken() - } - setVisible(!visible) + const regenerate = async () => { + if (!confirm('Regenerate token?')) return + await regenToken.fetch({ url: deploymentUrl }) + getDeployment.fetch({ url: deploymentUrl }) } return ( -
    - +
    +
    ) } -const ProjectSettingsPage = () => { - const p = project.data - const deps = deployments.data ?? [] - const { editing, saving } = url.params - const isEditing = editing === 'project' - const isSaving = saving === 'true' - - const handleSubmit = async (e: TargetedEvent) => { - e.preventDefault() - if (!p) return - - navigate({ params: { saving: 'true' }, replace: true }) - const formData = new FormData(e.currentTarget) - const name = formData.get('name') as string - const repositoryUrl = formData.get('repositoryUrl') as string - const isPublic = formData.get('isPublic') === 'on' - - try { - await api['PUT/api/project'].fetch({ - ...p, - name, - repositoryUrl: repositoryUrl || undefined, - isPublic, - }) - project.fetch({ slug: p.slug }) - navigate({ params: { editing: null, saving: null }, replace: true }) - } catch (err) { - console.error(err) - navigate({ params: { saving: null }, replace: true }) - } +// 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 ( -
    - -
    -
    -
    -
    -
    -

    Project Information

    - {isEditing ? ( + + + + + + + + + + + + + + + + Add + + } + > + {deps.length === 0 + ? ( +

    + No deployments configured. +

    + ) + : ( +
    + {deps.map((dep) => ( + + + {dep.url} +
    - - - - + {dep.logsEnabled && ( + Logs + )} + {dep.databaseEnabled && ( + DB + )}
    - ) : ( - - Edit - - )} -
    -
    - - - -
    - Visibility - {isEditing ? ( - - ) : ( - {p?.isPublic ? 'Public' : 'Private'} - )} -
    - - -
    + + ))}
    - - - - - - - - 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 ?? [] - const { url: selectedUrl, editing, saving } = url.params - const selectedDep = deps.find((d) => d.url === selectedUrl) - const isEditing = editing === 'deployment' - const isSaving = saving === 'true' - - const handleSubmit = async (e: TargetedEvent) => { - e.preventDefault() - if (!selectedDep || !project.data) return - - navigate({ params: { saving: 'true' }, replace: true }) - const formData = new FormData(e.currentTarget) - const logsEnabled = formData.get('logsEnabled') === 'on' - const databaseEnabled = formData.get('databaseEnabled') === 'on' - const sqlEndpoint = formData.get('sqlEndpoint') as string - const sqlToken = formData.get('sqlToken') as string + const deps = deployments.data ?? [], + dep = getDeployment.data, + selectedUrl = url.params.dep - try { - await api['PUT/api/deployment'].fetch({ - url: selectedDep.url, - projectId: project.data.slug, - logsEnabled, - databaseEnabled, - sqlEndpoint: sqlEndpoint || undefined, - sqlToken: sqlToken || undefined, - }) - deployments.fetch({ project: project.data.slug }) - navigate({ params: { editing: null, saving: null }, replace: true }) - } catch (err) { - console.error(err) - navigate({ params: { saving: null }, replace: true }) - } + if (!selectedUrl && deps.length > 0) { + navigate({ params: { dep: deps[0].url }, replace: true }) + return null } return ( -
    - + - + New - {deps.length > 0 && ( - - )} -
    -
    -
    -
    - {selectedDep && ( -
    -
    -
    -

    Deployment Configuration

    - {isEditing ? ( -
    - - - - -
    - ) : ( - - Edit - - )} -
    -
    - - -
    - Logs Enabled - {isEditing ? ( - - ) : ( - {selectedDep.logsEnabled ? 'Yes' : 'No'} - )} -
    - -
    - Database Enabled - {isEditing ? ( - - ) : ( - {selectedDep.databaseEnabled ? 'Yes' : 'No'} - )} -
    - - {isEditing && ( - <> -
    - SQL Endpoint - -
    -
    - SQL Token - -
    - - )} - - {selectedDep.logsEnabled && ( - - )} - - - -
    -
    -
    + {deps.length > 0 && ( + )} - - {/* Tools Section */} - {selectedDep && ( - -
    -
    -

    Column Encryptors

    -

    - Add JS encryptors to encrypt specific columns in your database tables. -

    -
    - No encryptors configured. Click to add one. -
    - -
    - -
    -

    Data Transformers

    -

    - Transform data before displaying or exporting. -

    -
    - No transformers configured. -
    - +
    + } + > + {dep && ( +
    + +
    + + + + + +
    +
    -
    - - )} - - {!selectedDep && deps.length > 0 && ( -
    - Select a deployment to view its configuration. + + +
    - )} +
    +
    + )} + {dep && ( + +
    + + +
    +
    + )} + {!dep && deps.length > 0 && ( +
    + Select a deployment to view its configuration.
    -
    -
    + )} + {getDeployment.pending && ( +
    + Loading deployment... +
    + )} + ) } const TeamSettingsPage = () => ( -
    - -
    -
    -
    - Team settings content will go here. -
    -
    + +
    + Team settings content will go here.
    -
    + ) const views = { @@ -641,7 +753,6 @@ export const SettingsPage = () => {
    ) } - const Content = views[view as keyof typeof views] ?? views.project return (
    @@ -649,7 +760,7 @@ export const SettingsPage = () => {
    - {project.data && } + {project.data && }
    ) }