Phase 11: remote UX & network attach (0.11.0)
- Live remote-stack logs over a WebSocket proxied through the central app to
the agent (/ws/agent-logs/{agent}/{stack}); agent gains a WS log endpoint.
- Deploy to a remote host from the UI: host selector in the New Stack editor
and template dialog; templates instantiate onto an agent via the proxy.
- Network attach/detach: expandable inspect view per network with
connect/disconnect + container picker; GET /{id}/containers, POST connect/disconnect.
- Remove dead pages/Placeholder.tsx.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
ec7e3e706f
commit
1931500c24
@@ -8,6 +8,7 @@ import { EditorHelperPanel } from "@/components/stacks/EditorHelperPanel";
|
||||
import { EnvEditor } from "@/components/env/EnvEditor";
|
||||
import { PortConflictDialog } from "@/components/stacks/PortConflictDialog";
|
||||
import { stacksApi } from "@/api/stacks";
|
||||
import { agentsApi } from "@/api/agents";
|
||||
import { portsApi, type PortConflict } from "@/api/ports";
|
||||
import { apiErrorMessage } from "@/api/client";
|
||||
import { useThemeStore } from "@/store/theme";
|
||||
@@ -38,6 +39,7 @@ export function StackEditor() {
|
||||
const [runCmd, setRunCmd] = useState("");
|
||||
const [conflicts, setConflicts] = useState<PortConflict[] | null>(null);
|
||||
const [checking, setChecking] = useState(false);
|
||||
const [host, setHost] = useState("local");
|
||||
|
||||
const existing = useQuery({
|
||||
queryKey: ["stack", id],
|
||||
@@ -45,6 +47,14 @@ export function StackEditor() {
|
||||
enabled: !isNew,
|
||||
});
|
||||
|
||||
const agents = useQuery({
|
||||
queryKey: ["agents"],
|
||||
queryFn: () => agentsApi.list(),
|
||||
enabled: isNew,
|
||||
});
|
||||
const onlineAgents = (agents.data ?? []).filter((a) => a.status === "online");
|
||||
const remote = isNew && host !== "local";
|
||||
|
||||
useEffect(() => {
|
||||
if (existing.data) {
|
||||
setName(existing.data.name);
|
||||
@@ -61,6 +71,21 @@ export function StackEditor() {
|
||||
}
|
||||
setSaving(true);
|
||||
try {
|
||||
// Remote host: create the stack on the agent, then optionally start it.
|
||||
if (remote) {
|
||||
const aid = Number(host);
|
||||
const created = await agentsApi.createStack(aid, { name, yaml, env });
|
||||
qc.invalidateQueries({ queryKey: ["agent-stacks", aid] });
|
||||
toast.success("Saved");
|
||||
if (deploy) {
|
||||
const t = toast.loading("Deploying…");
|
||||
await agentsApi.action(aid, created.id, "start");
|
||||
toast.success("Deployed ✓", { id: t });
|
||||
}
|
||||
navigate(`/hosts/${aid}/stacks/${created.id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
let stackId = id;
|
||||
if (isNew) {
|
||||
const created = await stacksApi.create({ name, description, yaml, env });
|
||||
@@ -85,6 +110,11 @@ export function StackEditor() {
|
||||
};
|
||||
|
||||
const onDeploy = async () => {
|
||||
// The local port-conflict check doesn't apply to remote hosts.
|
||||
if (remote) {
|
||||
save(true);
|
||||
return;
|
||||
}
|
||||
setChecking(true);
|
||||
try {
|
||||
const found = await portsApi.conflicts(yaml, id);
|
||||
@@ -128,6 +158,21 @@ export function StackEditor() {
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
/>
|
||||
{isNew && onlineAgents.length > 0 && (
|
||||
<select
|
||||
value={host}
|
||||
onChange={(e) => setHost(e.target.value)}
|
||||
className="rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm dark:border-slate-600 dark:bg-slate-800"
|
||||
title="Target host"
|
||||
>
|
||||
<option value="local">This host</option>
|
||||
{onlineAgents.map((a) => (
|
||||
<option key={a.id} value={String(a.id)}>
|
||||
{a.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
<Button variant="outline" onClick={() => setConvertOpen((v) => !v)}>
|
||||
<Wand2 className="h-4 w-4" /> Convert docker run
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user