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
@@ -4,10 +4,14 @@ import { useQuery } from "@tanstack/react-query";
|
||||
import { LayoutTemplate, Cpu, Package } from "lucide-react";
|
||||
import { Badge, Button, Card, Input, Spinner } from "@/components/ui";
|
||||
import { templatesApi, type TemplateDetail, type TemplateSummary } from "@/api/templates";
|
||||
import { agentsApi } from "@/api/agents";
|
||||
import { apiErrorMessage } from "@/api/client";
|
||||
import { useAuthStore } from "@/store/auth";
|
||||
import { toast } from "sonner";
|
||||
|
||||
const selectClass =
|
||||
"w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm dark:border-slate-600 dark:bg-slate-800";
|
||||
|
||||
export function Templates() {
|
||||
const isAdmin = useAuthStore((s) => s.user?.role === "admin");
|
||||
const [selected, setSelected] = useState<TemplateDetail | null>(null);
|
||||
@@ -78,8 +82,12 @@ function UseTemplateDialog({
|
||||
const [values, setValues] = useState<Record<string, string>>(
|
||||
Object.fromEntries(template.variables.map((v) => [v.name, v.default]))
|
||||
);
|
||||
const [host, setHost] = useState("local");
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const agents = useQuery({ queryKey: ["agents"], queryFn: () => agentsApi.list() });
|
||||
const onlineAgents = (agents.data ?? []).filter((a) => a.status === "online");
|
||||
|
||||
const create = async () => {
|
||||
if (!name.trim()) {
|
||||
toast.error("Stack name required");
|
||||
@@ -87,9 +95,11 @@ function UseTemplateDialog({
|
||||
}
|
||||
setBusy(true);
|
||||
try {
|
||||
const res = await templatesApi.instantiate(template.id, name, values);
|
||||
const agentId = host === "local" ? null : Number(host);
|
||||
const res = await templatesApi.instantiate(template.id, name, values, agentId);
|
||||
toast.success(`Stack '${res.name}' created`);
|
||||
navigate(`/stacks/${res.id}/edit`);
|
||||
if (res.agent_id != null) navigate(`/hosts/${res.agent_id}/stacks/${res.id}`);
|
||||
else navigate(`/stacks/${res.id}/edit`);
|
||||
} catch (e) {
|
||||
toast.error(apiErrorMessage(e));
|
||||
} finally {
|
||||
@@ -106,6 +116,19 @@ function UseTemplateDialog({
|
||||
<span className="text-xs font-medium text-slate-500">Stack name</span>
|
||||
<Input value={name} onChange={(e) => setName(e.target.value)} />
|
||||
</label>
|
||||
{onlineAgents.length > 0 && (
|
||||
<label className="block space-y-1">
|
||||
<span className="text-xs font-medium text-slate-500">Deploy to host</span>
|
||||
<select className={selectClass} value={host} onChange={(e) => setHost(e.target.value)}>
|
||||
<option value="local">This host</option>
|
||||
{onlineAgents.map((a) => (
|
||||
<option key={a.id} value={String(a.id)}>
|
||||
{a.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
)}
|
||||
{template.variables.map((v) => (
|
||||
<label key={v.name} className="block space-y-1">
|
||||
<span className="text-xs font-medium text-slate-500">
|
||||
|
||||
Reference in New Issue
Block a user