missions: extract MissionLivePane to stay under 1500-LoC CI budget
Previous push hit the gates job's file-size gate — MissionCanvas.tsx was 1517 lines (17 over). The Live Pane xterm subcomponent is cleanly separable (no shared state with the parent, just takes nodeId + visible props), so it lifts into its own file at zero behavior cost. frontend/src/components/dashboard/MissionLivePane.tsx (new, 106 LoC) frontend/src/components/dashboard/MissionCanvas.tsx (1517 → 1424) Also drops the xterm.css + useResilientTerminal imports from MissionCanvas since only MissionLivePane needs them now.
This commit is contained in:
@@ -29,13 +29,8 @@ import {
|
|||||||
type TemplateKind,
|
type TemplateKind,
|
||||||
} from "@/lib/api/missions";
|
} from "@/lib/api/missions";
|
||||||
import { MarkdownBlock } from "./MarkdownBlock";
|
import { MarkdownBlock } from "./MarkdownBlock";
|
||||||
|
import { MissionLivePane } from "./MissionLivePane";
|
||||||
import { MissionWizard } from "./MissionWizard";
|
import { MissionWizard } from "./MissionWizard";
|
||||||
import {
|
|
||||||
nodeHerdrConnector,
|
|
||||||
useResilientTerminal,
|
|
||||||
type TermMode,
|
|
||||||
} from "@/components/computer/apps/terminal/core";
|
|
||||||
import "@xterm/xterm/css/xterm.css";
|
|
||||||
|
|
||||||
const mono =
|
const mono =
|
||||||
"ui-monospace, SFMono-Regular, SF Mono, Menlo, Monaco, Consolas, monospace";
|
"ui-monospace, SFMono-Regular, SF Mono, Menlo, Monaco, Consolas, monospace";
|
||||||
@@ -935,7 +930,7 @@ export function MissionCanvas({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{tab === "pane" && mission.runtime_kind === "local_herdr" && (
|
{tab === "pane" && mission.runtime_kind === "local_herdr" && (
|
||||||
<LivePane
|
<MissionLivePane
|
||||||
nodeId={mission.target_node_id}
|
nodeId={mission.target_node_id}
|
||||||
visible={tab === "pane"}
|
visible={tab === "pane"}
|
||||||
/>
|
/>
|
||||||
@@ -992,94 +987,6 @@ function Empty({ label }: { label: string }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function LivePane({
|
|
||||||
nodeId,
|
|
||||||
visible,
|
|
||||||
}: {
|
|
||||||
nodeId: string | null;
|
|
||||||
visible: boolean;
|
|
||||||
}) {
|
|
||||||
if (!nodeId) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
padding: 40,
|
|
||||||
textAlign: "center",
|
|
||||||
color: "#8a8a92",
|
|
||||||
fontSize: 13,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
This mission has no target node.
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return <LivePaneInner nodeId={nodeId} visible={visible} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
function LivePaneInner({
|
|
||||||
nodeId,
|
|
||||||
visible,
|
|
||||||
}: {
|
|
||||||
nodeId: string;
|
|
||||||
visible: boolean;
|
|
||||||
}) {
|
|
||||||
const [mode, setMode] = useState<TermMode>("connecting");
|
|
||||||
const { hostRef, refit } = useResilientTerminal(
|
|
||||||
{
|
|
||||||
connect: nodeHerdrConnector(nodeId, setMode),
|
|
||||||
autoFocus: false,
|
|
||||||
visible: () => visible,
|
|
||||||
},
|
|
||||||
[nodeId],
|
|
||||||
);
|
|
||||||
useEffect(() => {
|
|
||||||
if (visible) refit();
|
|
||||||
}, [visible, refit]);
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: "relative",
|
|
||||||
height: "70vh",
|
|
||||||
minHeight: 480,
|
|
||||||
background: "#0a0a0d",
|
|
||||||
borderRadius: 10,
|
|
||||||
border: "1px solid rgba(255,255,255,.06)",
|
|
||||||
overflow: "hidden",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
top: 8,
|
|
||||||
right: 8,
|
|
||||||
zIndex: 5,
|
|
||||||
padding: "2px 8px",
|
|
||||||
borderRadius: 6,
|
|
||||||
fontFamily: mono,
|
|
||||||
fontSize: 10,
|
|
||||||
letterSpacing: ".1em",
|
|
||||||
textTransform: "uppercase",
|
|
||||||
color:
|
|
||||||
mode === "direct"
|
|
||||||
? "#5fd08a"
|
|
||||||
: mode === "relayed"
|
|
||||||
? "#8a8a92"
|
|
||||||
: "#e8b465",
|
|
||||||
background:
|
|
||||||
mode === "direct"
|
|
||||||
? "rgba(95,208,138,.12)"
|
|
||||||
: "rgba(255,255,255,.05)",
|
|
||||||
border: `1px solid ${
|
|
||||||
mode === "direct" ? "rgba(95,208,138,.3)" : "rgba(255,255,255,.1)"
|
|
||||||
}`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{mode === "direct" ? "direct" : mode === "relayed" ? "relayed" : "connecting…"}
|
|
||||||
</div>
|
|
||||||
<div ref={hostRef} style={{ position: "absolute", inset: 0, padding: 8 }} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function EditMissionModal({
|
function EditMissionModal({
|
||||||
mission,
|
mission,
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
// Live Pane tab body for missions running on a fleet node via Herdr.
|
||||||
|
// Attaches xterm.js to the node's Herdr TUI (WebRTC direct → WS-relay
|
||||||
|
// fallback). Rendered inline in MissionCanvas when
|
||||||
|
// mission.runtime_kind='local_herdr' and tab='pane'.
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
nodeHerdrConnector,
|
||||||
|
useResilientTerminal,
|
||||||
|
type TermMode,
|
||||||
|
} from "@/components/computer/apps/terminal/core";
|
||||||
|
import "@xterm/xterm/css/xterm.css";
|
||||||
|
|
||||||
|
const mono =
|
||||||
|
"ui-monospace, SFMono-Regular, SF Mono, Menlo, Monaco, Consolas, monospace";
|
||||||
|
|
||||||
|
export function MissionLivePane({
|
||||||
|
nodeId,
|
||||||
|
visible,
|
||||||
|
}: {
|
||||||
|
nodeId: string | null;
|
||||||
|
visible: boolean;
|
||||||
|
}) {
|
||||||
|
if (!nodeId) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: 40,
|
||||||
|
textAlign: "center",
|
||||||
|
color: "#8a8a92",
|
||||||
|
fontSize: 13,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
This mission has no target node.
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <LivePaneInner nodeId={nodeId} visible={visible} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function LivePaneInner({
|
||||||
|
nodeId,
|
||||||
|
visible,
|
||||||
|
}: {
|
||||||
|
nodeId: string;
|
||||||
|
visible: boolean;
|
||||||
|
}) {
|
||||||
|
const [mode, setMode] = useState<TermMode>("connecting");
|
||||||
|
const { hostRef, refit } = useResilientTerminal(
|
||||||
|
{
|
||||||
|
connect: nodeHerdrConnector(nodeId, setMode),
|
||||||
|
autoFocus: false,
|
||||||
|
visible: () => visible,
|
||||||
|
},
|
||||||
|
[nodeId],
|
||||||
|
);
|
||||||
|
useEffect(() => {
|
||||||
|
if (visible) refit();
|
||||||
|
}, [visible, refit]);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "relative",
|
||||||
|
height: "70vh",
|
||||||
|
minHeight: 480,
|
||||||
|
background: "#0a0a0d",
|
||||||
|
borderRadius: 10,
|
||||||
|
border: "1px solid rgba(255,255,255,.06)",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 8,
|
||||||
|
right: 8,
|
||||||
|
zIndex: 5,
|
||||||
|
padding: "2px 8px",
|
||||||
|
borderRadius: 6,
|
||||||
|
fontFamily: mono,
|
||||||
|
fontSize: 10,
|
||||||
|
letterSpacing: ".1em",
|
||||||
|
textTransform: "uppercase",
|
||||||
|
color:
|
||||||
|
mode === "direct"
|
||||||
|
? "#5fd08a"
|
||||||
|
: mode === "relayed"
|
||||||
|
? "#8a8a92"
|
||||||
|
: "#e8b465",
|
||||||
|
background:
|
||||||
|
mode === "direct"
|
||||||
|
? "rgba(95,208,138,.12)"
|
||||||
|
: "rgba(255,255,255,.05)",
|
||||||
|
border: `1px solid ${
|
||||||
|
mode === "direct" ? "rgba(95,208,138,.3)" : "rgba(255,255,255,.1)"
|
||||||
|
}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{mode === "direct" ? "direct" : mode === "relayed" ? "relayed" : "connecting…"}
|
||||||
|
</div>
|
||||||
|
<div ref={hostRef} style={{ position: "absolute", inset: 0, padding: 8 }} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user