Dashboard UI: subdomain checkbox, drop proxy path/host
Follow the backend to the subdomain-only model:
- Service editor + create form: replace the Proxy path / Proxy host text fields
with a single "Expose" checkbox (writes proxy: { caddy: {} } or removes proxy).
- ServiceSummary/DeploymentSummary type: proxy_path/proxy_host → subdomain.
- ServiceCard / ServiceDetail: show the subdomain, linking to
<subdomain>.<domain> via a new subdomainUrl() helper (domain derived from the
dashboard's own host; null on a bare host / off mode).
This commit is contained in:
@@ -2,7 +2,7 @@ import { ExternalLink, Play, RefreshCw, Server, Square, Terminal } from "lucide-
|
||||
import { Link } from "react-router-dom"
|
||||
import type { ServiceSummary, HealthStatus } from "@/types"
|
||||
import { useServiceAction } from "@/services/api/hooks"
|
||||
import { runnerLabel } from "@/lib/labels"
|
||||
import { runnerLabel, subdomainUrl } from "@/lib/labels"
|
||||
import { HealthBadge } from "./HealthBadge"
|
||||
import { StackBadge } from "./StackBadge"
|
||||
|
||||
@@ -58,13 +58,13 @@ export function ServiceCard({ service, health }: ServiceCardProps) {
|
||||
{runnerLabel(service.runner)}
|
||||
</span>
|
||||
)}
|
||||
{service.proxy_path && (
|
||||
{service.subdomain && (
|
||||
<a
|
||||
href={service.proxy_path + "/"}
|
||||
href={subdomainUrl(service.subdomain) ?? undefined}
|
||||
className="flex items-center gap-1 text-[var(--primary)] hover:underline"
|
||||
>
|
||||
<ExternalLink size={12} />
|
||||
{service.proxy_path}
|
||||
{service.subdomain}
|
||||
</a>
|
||||
)}
|
||||
{service.port && (
|
||||
|
||||
@@ -39,8 +39,7 @@ export function CreateDeploymentForm({
|
||||
const [runTarget, setRunTarget] = useState(prefill?.runTarget ?? prefill?.name ?? "")
|
||||
const [port, setPort] = useState("")
|
||||
const [health, setHealth] = useState("/health")
|
||||
const [path, setPath] = useState("")
|
||||
const [host, setHost] = useState("")
|
||||
const [expose, setExpose] = useState(true)
|
||||
const [schedule, setSchedule] = useState("0 2 * * *")
|
||||
const [busy, setBusy] = useState<string | null>(null)
|
||||
const [error, setError] = useState("")
|
||||
@@ -75,14 +74,7 @@ export function CreateDeploymentForm({
|
||||
},
|
||||
}
|
||||
}
|
||||
if (path || host) {
|
||||
base.proxy = {
|
||||
caddy: {
|
||||
...(path ? { path_prefix: path.startsWith("/") ? path : `/${path}` } : {}),
|
||||
...(host ? { host } : {}),
|
||||
},
|
||||
}
|
||||
}
|
||||
if (port && expose) base.proxy = { caddy: {} }
|
||||
return base
|
||||
}
|
||||
|
||||
@@ -160,8 +152,14 @@ export function CreateDeploymentForm({
|
||||
<>
|
||||
<TextField label="Port" value={port} onChange={setPort} width="w-32" mono placeholder="9001" />
|
||||
<TextField label="Health path" value={health} onChange={setHealth} width="w-48" mono />
|
||||
<TextField label="Proxy path" value={path} onChange={setPath} width="w-48" mono placeholder={`/${name || "name"}`} />
|
||||
<TextField label="Proxy host" value={host} onChange={setHost} mono placeholder="my-service.lan (optional)" />
|
||||
<Field label="Expose" hint="Route through the gateway at <name>.<gateway.domain>. Off: reachable only at host:port.">
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||
<input type="checkbox" checked={expose} onChange={(e) => setExpose(e.target.checked)} />
|
||||
<span className="font-mono text-[var(--muted)]">
|
||||
{expose ? `${name || "name"}.<gateway.domain>` : "off (host:port only)"}
|
||||
</span>
|
||||
</label>
|
||||
</Field>
|
||||
</>
|
||||
) : (
|
||||
<TextField label="Schedule" value={schedule} onChange={setSchedule} width="w-48" mono placeholder="0 2 * * *" />
|
||||
|
||||
@@ -18,7 +18,7 @@ export function ServiceFields({ service, onSave, onDelete }: Props) {
|
||||
const run = obj(m.run)
|
||||
const internal = obj(obj(obj(m.expose).http).internal)
|
||||
const httpExpose = obj(obj(m.expose).http)
|
||||
const caddy = obj(obj(m.proxy).caddy)
|
||||
const caddyRaw = obj(m.proxy).caddy
|
||||
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [saved, setSaved] = useState(false)
|
||||
@@ -29,8 +29,8 @@ export function ServiceFields({ service, onSave, onDelete }: Props) {
|
||||
)
|
||||
const [port, setPort] = useState(internal.port != null ? String(internal.port) : "")
|
||||
const [health, setHealth] = useState((httpExpose.health_path as string) ?? "")
|
||||
const [proxyPath, setProxyPath] = useState((caddy.path_prefix as string) ?? "")
|
||||
const [proxyHost, setProxyHost] = useState((caddy.host as string) ?? "")
|
||||
// Exposed at <service-name>.<gateway.domain> when proxy.caddy is present + enabled.
|
||||
const [expose, setExpose] = useState(caddyRaw !== undefined && obj(caddyRaw).enable !== false)
|
||||
|
||||
const { element: envEditor, merged } = useEnvSecrets(obj(obj(m.defaults).env) as Record<string, string>)
|
||||
|
||||
@@ -62,16 +62,8 @@ export function ServiceFields({ service, onSave, onDelete }: Props) {
|
||||
delete config.expose
|
||||
}
|
||||
|
||||
if (proxyPath || proxyHost) {
|
||||
config.proxy = {
|
||||
caddy: {
|
||||
...(proxyPath ? { path_prefix: proxyPath } : {}),
|
||||
...(proxyHost ? { host: proxyHost } : {}),
|
||||
},
|
||||
}
|
||||
} else {
|
||||
delete config.proxy
|
||||
}
|
||||
if (expose) config.proxy = { caddy: {} }
|
||||
else delete config.proxy
|
||||
|
||||
const env = merged()
|
||||
if (Object.keys(env).length > 0) config.defaults = { ...obj(config.defaults), env }
|
||||
@@ -117,23 +109,21 @@ export function ServiceFields({ service, onSave, onDelete }: Props) {
|
||||
placeholder="/health"
|
||||
hint="HTTP path castle polls to report up/down."
|
||||
/>
|
||||
<TextField
|
||||
label="Proxy path"
|
||||
value={proxyPath}
|
||||
onChange={setProxyPath}
|
||||
width="w-48"
|
||||
mono
|
||||
placeholder="/my-service"
|
||||
hint="Gateway prefix — reachable at gateway:9000<path>/ (reverse-proxied to the port)."
|
||||
/>
|
||||
<TextField
|
||||
label="Proxy host"
|
||||
value={proxyHost}
|
||||
onChange={setProxyHost}
|
||||
mono
|
||||
placeholder="my-service.lan"
|
||||
hint="Optional: route a whole hostname to this service instead of a path (lets a root-based app serve unchanged)."
|
||||
/>
|
||||
<Field
|
||||
label="Expose"
|
||||
hint="Route this service through the gateway at <service-name>.<gateway.domain>. Unchecked: reachable only at its own host:port."
|
||||
>
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={expose}
|
||||
onChange={(e) => setExpose(e.target.checked)}
|
||||
/>
|
||||
<span className="font-mono text-[var(--muted)]">
|
||||
{expose ? `${service.id}.<gateway.domain>` : "off (host:port only)"}
|
||||
</span>
|
||||
</label>
|
||||
</Field>
|
||||
{envEditor}
|
||||
<FormFooter
|
||||
saving={saving}
|
||||
|
||||
Reference in New Issue
Block a user