mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-12-16 06:45:48 -07:00
Remote Shell UI Changes
This commit is contained in:
@@ -4,7 +4,6 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
Button,
|
Button,
|
||||||
Stack,
|
Stack,
|
||||||
Chip,
|
|
||||||
TextField,
|
TextField,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
IconButton,
|
IconButton,
|
||||||
@@ -18,7 +17,6 @@ import {
|
|||||||
ContentCopy as CopyIcon,
|
ContentCopy as CopyIcon,
|
||||||
RefreshRounded as RefreshIcon,
|
RefreshRounded as RefreshIcon,
|
||||||
LanRounded as PortIcon,
|
LanRounded as PortIcon,
|
||||||
SensorsRounded as ActivityIcon,
|
|
||||||
LinkRounded as LinkIcon,
|
LinkRounded as LinkIcon,
|
||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
import { io } from "socket.io-client";
|
import { io } from "socket.io-client";
|
||||||
@@ -108,13 +106,11 @@ function highlightPs(code) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const INITIAL_MILESTONES = {
|
const INITIAL_MILESTONES = {
|
||||||
requested: false,
|
tunnelReady: false,
|
||||||
leaseIssued: false,
|
operatorAttached: false,
|
||||||
operatorJoined: false,
|
shellEstablished: false,
|
||||||
channelOpened: false,
|
|
||||||
ack: false,
|
|
||||||
active: false,
|
|
||||||
};
|
};
|
||||||
|
const INITIAL_STATUS_CHAIN = ["Offline"];
|
||||||
|
|
||||||
export default function ReverseTunnelPowershell({ device }) {
|
export default function ReverseTunnelPowershell({ device }) {
|
||||||
const [connectionType, setConnectionType] = useState("ps");
|
const [connectionType, setConnectionType] = useState("ps");
|
||||||
@@ -127,8 +123,10 @@ export default function ReverseTunnelPowershell({ device }) {
|
|||||||
const [copyFlash, setCopyFlash] = useState(false);
|
const [copyFlash, setCopyFlash] = useState(false);
|
||||||
const [, setPolling] = useState(false);
|
const [, setPolling] = useState(false);
|
||||||
const [psStatus, setPsStatus] = useState({});
|
const [psStatus, setPsStatus] = useState({});
|
||||||
const [statusLine, setStatusLine] = useState("Idle");
|
|
||||||
const [milestones, setMilestones] = useState(() => ({ ...INITIAL_MILESTONES }));
|
const [milestones, setMilestones] = useState(() => ({ ...INITIAL_MILESTONES }));
|
||||||
|
const [tunnelSteps, setTunnelSteps] = useState(() => [...INITIAL_STATUS_CHAIN]);
|
||||||
|
const [websocketSteps, setWebsocketSteps] = useState(() => [...INITIAL_STATUS_CHAIN]);
|
||||||
|
const [shellSteps, setShellSteps] = useState(() => [...INITIAL_STATUS_CHAIN]);
|
||||||
const socketRef = useRef(null);
|
const socketRef = useRef(null);
|
||||||
const pollTimerRef = useRef(null);
|
const pollTimerRef = useRef(null);
|
||||||
const resizeTimerRef = useRef(null);
|
const resizeTimerRef = useRef(null);
|
||||||
@@ -166,6 +164,15 @@ export default function ReverseTunnelPowershell({ device }) {
|
|||||||
);
|
);
|
||||||
}, [device]);
|
}, [device]);
|
||||||
|
|
||||||
|
const appendStatus = useCallback((setter, label) => {
|
||||||
|
if (!label) return;
|
||||||
|
setter((prev) => {
|
||||||
|
const next = [...prev, label];
|
||||||
|
const cap = 6;
|
||||||
|
return next.length > cap ? next.slice(next.length - cap) : next;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
const resetState = useCallback(() => {
|
const resetState = useCallback(() => {
|
||||||
debugLog("resetState invoked");
|
debugLog("resetState invoked");
|
||||||
setTunnel(null);
|
setTunnel(null);
|
||||||
@@ -176,7 +183,9 @@ export default function ReverseTunnelPowershell({ device }) {
|
|||||||
setInput("");
|
setInput("");
|
||||||
setPsStatus({});
|
setPsStatus({});
|
||||||
setMilestones({ ...INITIAL_MILESTONES });
|
setMilestones({ ...INITIAL_MILESTONES });
|
||||||
setStatusLine("Idle");
|
setTunnelSteps([...INITIAL_STATUS_CHAIN]);
|
||||||
|
setWebsocketSteps([...INITIAL_STATUS_CHAIN]);
|
||||||
|
setShellSteps([...INITIAL_STATUS_CHAIN]);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -203,7 +212,6 @@ export default function ReverseTunnelPowershell({ device }) {
|
|||||||
const stopTunnel = useCallback(async (reason = "operator_disconnect", tunnelIdOverride = null) => {
|
const stopTunnel = useCallback(async (reason = "operator_disconnect", tunnelIdOverride = null) => {
|
||||||
const tunnelId = tunnelIdOverride || tunnelRef.current;
|
const tunnelId = tunnelIdOverride || tunnelRef.current;
|
||||||
if (!tunnelId) return;
|
if (!tunnelId) return;
|
||||||
setStatusLine(`Stopping tunnel ${tunnelId} reason=${reason}`);
|
|
||||||
try {
|
try {
|
||||||
await fetch(`/api/tunnel/${tunnelId}`, {
|
await fetch(`/api/tunnel/${tunnelId}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
@@ -292,7 +300,6 @@ export default function ReverseTunnelPowershell({ device }) {
|
|||||||
const pollLoop = useCallback(
|
const pollLoop = useCallback(
|
||||||
(socket, tunnelId) => {
|
(socket, tunnelId) => {
|
||||||
if (!socket || !tunnelId) return;
|
if (!socket || !tunnelId) return;
|
||||||
setStatusLine(`Polling tunnel ${tunnelId}`);
|
|
||||||
debugLog("pollLoop tick", { tunnelId });
|
debugLog("pollLoop tick", { tunnelId });
|
||||||
setPolling(true);
|
setPolling(true);
|
||||||
pollTimerRef.current = setTimeout(async () => {
|
pollTimerRef.current = setTimeout(async () => {
|
||||||
@@ -313,33 +320,33 @@ export default function ReverseTunnelPowershell({ device }) {
|
|||||||
setPsStatus(resp.status);
|
setPsStatus(resp.status);
|
||||||
debugLog("pollLoop status", resp.status);
|
debugLog("pollLoop status", resp.status);
|
||||||
if (resp.status.closed) {
|
if (resp.status.closed) {
|
||||||
setStatusLine(`Tunnel ${tunnelId} reported closed`);
|
|
||||||
setSessionState("closed");
|
setSessionState("closed");
|
||||||
setTunnel(null);
|
setTunnel(null);
|
||||||
setMilestones({ ...INITIAL_MILESTONES });
|
setMilestones({ ...INITIAL_MILESTONES });
|
||||||
|
appendStatus(setShellSteps, "Shell closed");
|
||||||
|
appendStatus(setTunnelSteps, "Stopped");
|
||||||
|
appendStatus(setWebsocketSteps, "Relay stopped");
|
||||||
stopPolling();
|
stopPolling();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (resp.status.open_sent) {
|
if (resp.status.open_sent) {
|
||||||
setMilestones((prev) => ({ ...prev, channelOpened: true }));
|
appendStatus(setShellSteps, "Opening remote shell");
|
||||||
setStatusLine("Remote shell opening…");
|
|
||||||
}
|
}
|
||||||
if (resp.status.ack) {
|
if (resp.status.ack) {
|
||||||
setSessionState("connected");
|
setSessionState("connected");
|
||||||
setMilestones((prev) => ({ ...prev, ack: true, active: true }));
|
setMilestones((prev) => ({ ...prev, shellEstablished: true }));
|
||||||
setStatusLine("Remote shell active");
|
appendStatus(setShellSteps, "Remote shell established");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pollLoop(socket, tunnelId);
|
pollLoop(socket, tunnelId);
|
||||||
}, 520);
|
}, 520);
|
||||||
},
|
},
|
||||||
[appendOutput, emitAsync, stopPolling, disconnectSocket]
|
[appendOutput, emitAsync, stopPolling, disconnectSocket, appendStatus]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDisconnect = useCallback(
|
const handleDisconnect = useCallback(
|
||||||
async (reason = "operator_disconnect") => {
|
async (reason = "operator_disconnect") => {
|
||||||
debugLog("handleDisconnect begin", { reason, tunnelId: tunnel?.tunnel_id, psStatus, sessionState });
|
debugLog("handleDisconnect begin", { reason, tunnelId: tunnel?.tunnel_id, psStatus, sessionState });
|
||||||
setStatusLine(`Disconnect requested (${reason})`);
|
|
||||||
setPsStatus({});
|
setPsStatus({});
|
||||||
const socket = socketRef.current;
|
const socket = socketRef.current;
|
||||||
const tunnelId = tunnel?.tunnel_id;
|
const tunnelId = tunnel?.tunnel_id;
|
||||||
@@ -360,10 +367,12 @@ export default function ReverseTunnelPowershell({ device }) {
|
|||||||
setTunnel(null);
|
setTunnel(null);
|
||||||
setSessionState("closed");
|
setSessionState("closed");
|
||||||
setMilestones({ ...INITIAL_MILESTONES });
|
setMilestones({ ...INITIAL_MILESTONES });
|
||||||
setStatusLine("Disconnected");
|
appendStatus(setTunnelSteps, "Stopped");
|
||||||
|
appendStatus(setWebsocketSteps, "Relay closed");
|
||||||
|
appendStatus(setShellSteps, "Shell closed");
|
||||||
debugLog("handleDisconnect finished", { tunnelId });
|
debugLog("handleDisconnect finished", { tunnelId });
|
||||||
},
|
},
|
||||||
[disconnectSocket, stopPolling, stopTunnel, tunnel?.tunnel_id]
|
[appendStatus, disconnectSocket, stopPolling, stopTunnel, tunnel?.tunnel_id]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleResize = useCallback(() => {
|
const handleResize = useCallback(() => {
|
||||||
@@ -405,19 +414,17 @@ export default function ReverseTunnelPowershell({ device }) {
|
|||||||
setSessionState("waiting");
|
setSessionState("waiting");
|
||||||
const socket = io(`${window.location.origin}/tunnel`, { transports: ["websocket", "polling"] });
|
const socket = io(`${window.location.origin}/tunnel`, { transports: ["websocket", "polling"] });
|
||||||
socketRef.current = socket;
|
socketRef.current = socket;
|
||||||
setStatusLine("Connecting to tunnel namespace…");
|
|
||||||
|
|
||||||
socket.on("connect_error", () => {
|
socket.on("connect_error", () => {
|
||||||
setStatusLine("Socket connect error");
|
|
||||||
debugLog("socket connect_error");
|
debugLog("socket connect_error");
|
||||||
setStatusSeverity("warning");
|
setStatusSeverity("warning");
|
||||||
setStatusMessage("Tunnel namespace unavailable.");
|
setStatusMessage("Tunnel namespace unavailable.");
|
||||||
setTunnel(null);
|
setTunnel(null);
|
||||||
setSessionState("error");
|
setSessionState("error");
|
||||||
|
appendStatus(setWebsocketSteps, "Relay connect error");
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("disconnect", () => {
|
socket.on("disconnect", () => {
|
||||||
setStatusLine("Socket disconnected");
|
|
||||||
debugLog("socket disconnect", { tunnelId: tunnel?.tunnel_id });
|
debugLog("socket disconnect", { tunnelId: tunnel?.tunnel_id });
|
||||||
stopPolling();
|
stopPolling();
|
||||||
if (sessionState !== "closed") {
|
if (sessionState !== "closed") {
|
||||||
@@ -425,15 +432,16 @@ export default function ReverseTunnelPowershell({ device }) {
|
|||||||
setStatusSeverity("warning");
|
setStatusSeverity("warning");
|
||||||
setStatusMessage("Socket disconnected.");
|
setStatusMessage("Socket disconnected.");
|
||||||
setTunnel(null);
|
setTunnel(null);
|
||||||
|
appendStatus(setWebsocketSteps, "Relay disconnected");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("connect", async () => {
|
socket.on("connect", async () => {
|
||||||
debugLog("socket connect", { tunnelId: lease.tunnel_id });
|
debugLog("socket connect", { tunnelId: lease.tunnel_id });
|
||||||
setMilestones((prev) => ({ ...prev, operatorJoined: true }));
|
setMilestones((prev) => ({ ...prev, operatorAttached: true }));
|
||||||
setStatusLine("Operator attached; joining tunnel…");
|
|
||||||
setStatusSeverity("info");
|
setStatusSeverity("info");
|
||||||
setStatusMessage("Joining tunnel...");
|
setStatusMessage("Joining tunnel...");
|
||||||
|
appendStatus(setWebsocketSteps, "Relay connected");
|
||||||
const joinResp = await emitAsync(socket, "join", { tunnel_id: lease.tunnel_id }, 5000);
|
const joinResp = await emitAsync(socket, "join", { tunnel_id: lease.tunnel_id }, 5000);
|
||||||
if (joinResp?.error) {
|
if (joinResp?.error) {
|
||||||
const attempt = (joinAttemptsRef.current += 1);
|
const attempt = (joinAttemptsRef.current += 1);
|
||||||
@@ -442,18 +450,18 @@ export default function ReverseTunnelPowershell({ device }) {
|
|||||||
setSessionState("waiting_agent");
|
setSessionState("waiting_agent");
|
||||||
setStatusSeverity("info");
|
setStatusSeverity("info");
|
||||||
setStatusMessage("Waiting for agent to establish tunnel...");
|
setStatusMessage("Waiting for agent to establish tunnel...");
|
||||||
setStatusLine("Waiting for agent to attach to tunnel…");
|
appendStatus(setWebsocketSteps, "Waiting for agent");
|
||||||
} else if (isTimeout || joinResp.error === "attach_failed") {
|
} else if (isTimeout || joinResp.error === "attach_failed") {
|
||||||
setSessionState("waiting_agent");
|
setSessionState("waiting_agent");
|
||||||
setStatusSeverity("warning");
|
setStatusSeverity("warning");
|
||||||
setStatusMessage("Tunnel join timed out. Retrying...");
|
setStatusMessage("Tunnel join timed out. Retrying...");
|
||||||
setStatusLine(`Join timed out (attempt ${attempt}/5); retrying…`);
|
appendStatus(setWebsocketSteps, `Join retry ${attempt}`);
|
||||||
} else {
|
} else {
|
||||||
debugLog("join error", joinResp);
|
debugLog("join error", joinResp);
|
||||||
setSessionState("error");
|
setSessionState("error");
|
||||||
setStatusSeverity("error");
|
setStatusSeverity("error");
|
||||||
setStatusMessage(joinResp.error);
|
setStatusMessage(joinResp.error);
|
||||||
setStatusLine(`Join failed: ${joinResp.error}`);
|
appendStatus(setWebsocketSteps, `Join failed: ${joinResp.error}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (attempt <= 5) {
|
if (attempt <= 5) {
|
||||||
@@ -463,36 +471,35 @@ export default function ReverseTunnelPowershell({ device }) {
|
|||||||
setTunnel(null);
|
setTunnel(null);
|
||||||
setStatusSeverity("warning");
|
setStatusSeverity("warning");
|
||||||
setStatusMessage("Operator could not attach to tunnel. Try Connect again.");
|
setStatusMessage("Operator could not attach to tunnel. Try Connect again.");
|
||||||
setStatusLine("Operator attach failed after retries");
|
appendStatus(setWebsocketSteps, "Join failed after retries");
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
appendStatus(setWebsocketSteps, "Relay joined");
|
||||||
const dims = measureTerminal();
|
const dims = measureTerminal();
|
||||||
debugLog("ps_open emit", { tunnelId: lease.tunnel_id, dims });
|
debugLog("ps_open emit", { tunnelId: lease.tunnel_id, dims });
|
||||||
setMilestones((prev) => ({ ...prev, channelOpened: true }));
|
|
||||||
setStatusLine("Opening remote shell…");
|
|
||||||
const openResp = await emitAsync(socket, "ps_open", dims, 5000);
|
const openResp = await emitAsync(socket, "ps_open", dims, 5000);
|
||||||
if (openResp?.error && openResp.error === "ps_unsupported") {
|
if (openResp?.error && openResp.error === "ps_unsupported") {
|
||||||
// Suppress warming message; channel will settle once agent attaches.
|
// Suppress warming message; channel will settle once agent attaches.
|
||||||
}
|
}
|
||||||
|
appendStatus(setShellSteps, "Opening remote shell");
|
||||||
appendOutput("");
|
appendOutput("");
|
||||||
setSessionState("waiting_agent");
|
setSessionState("waiting_agent");
|
||||||
pollLoop(socket, lease.tunnel_id);
|
pollLoop(socket, lease.tunnel_id);
|
||||||
handleResize();
|
handleResize();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[appendOutput, disconnectSocket, emitAsync, handleResize, measureTerminal, pollLoop, sessionState, stopPolling]
|
[appendOutput, appendStatus, disconnectSocket, emitAsync, handleResize, measureTerminal, pollLoop, sessionState, stopPolling]
|
||||||
);
|
);
|
||||||
|
|
||||||
const requestTunnel = useCallback(async () => {
|
const requestTunnel = useCallback(async () => {
|
||||||
if (tunnel && sessionState !== "closed" && sessionState !== "idle") {
|
if (tunnel && sessionState !== "closed" && sessionState !== "idle") {
|
||||||
setStatusSeverity("info");
|
setStatusSeverity("info");
|
||||||
setStatusMessage("");
|
setStatusMessage("");
|
||||||
connectSocket(tunnel);
|
connectSocket(tunnel);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
debugLog("requestTunnel", { agentId, connectionType });
|
debugLog("requestTunnel", { agentId, connectionType });
|
||||||
setStatusLine("Requesting tunnel…");
|
|
||||||
if (!agentId) {
|
if (!agentId) {
|
||||||
setStatusSeverity("warning");
|
setStatusSeverity("warning");
|
||||||
setStatusMessage("Agent ID is required to request a tunnel.");
|
setStatusMessage("Agent ID is required to request a tunnel.");
|
||||||
@@ -507,6 +514,7 @@ export default function ReverseTunnelPowershell({ device }) {
|
|||||||
setSessionState("requesting");
|
setSessionState("requesting");
|
||||||
setStatusSeverity("info");
|
setStatusSeverity("info");
|
||||||
setStatusMessage("");
|
setStatusMessage("");
|
||||||
|
appendStatus(setTunnelSteps, "Requesting lease");
|
||||||
try {
|
try {
|
||||||
const resp = await fetch("/api/tunnel/request", {
|
const resp = await fetch("/api/tunnel/request", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -521,19 +529,24 @@ export default function ReverseTunnelPowershell({ device }) {
|
|||||||
setStatusMessage("");
|
setStatusMessage("");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setMilestones((prev) => ({ ...prev, requested: true, leaseIssued: true }));
|
setMilestones((prev) => ({ ...prev, tunnelReady: true }));
|
||||||
setTunnel(data);
|
setTunnel(data);
|
||||||
setStatusMessage("");
|
setStatusMessage("");
|
||||||
setSessionState("lease_issued");
|
setSessionState("lease_issued");
|
||||||
setStatusLine(`Lease issued for tunnel ${data.tunnel_id || "-"}`);
|
appendStatus(
|
||||||
|
setTunnelSteps,
|
||||||
|
data?.tunnel_id
|
||||||
|
? `Lease issued (${data.tunnel_id.slice(0, 8)} @ Port ${data.port || "-"})`
|
||||||
|
: "Lease issued"
|
||||||
|
);
|
||||||
connectSocket(data);
|
connectSocket(data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setSessionState("error");
|
setSessionState("error");
|
||||||
setStatusSeverity("error");
|
setStatusSeverity("error");
|
||||||
setStatusMessage("");
|
setStatusMessage("");
|
||||||
setStatusLine("Tunnel request failed");
|
appendStatus(setTunnelSteps, "Lease request failed");
|
||||||
}
|
}
|
||||||
}, [DOMAIN_REMOTE_SHELL, agentId, connectSocket, connectionType, resetState]);
|
}, [DOMAIN_REMOTE_SHELL, agentId, appendStatus, connectSocket, connectionType, resetState]);
|
||||||
|
|
||||||
const handleSend = useCallback(
|
const handleSend = useCallback(
|
||||||
async (text) => {
|
async (text) => {
|
||||||
@@ -640,70 +653,44 @@ export default function ReverseTunnelPowershell({ device }) {
|
|||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
{/* Workflow pills */}
|
<Stack spacing={0.3} sx={{ mt: 1.25 }}>
|
||||||
<Box sx={{ mt: 1.5, display: "flex", flexWrap: "wrap", alignItems: "center", gap: 0.75 }}>
|
<Typography
|
||||||
{[
|
variant="body2"
|
||||||
{ key: "requested", label: "Establish Tunnel" },
|
sx={{
|
||||||
{
|
color: milestones.tunnelReady ? MAGIC_UI.accentC : MAGIC_UI.textMuted,
|
||||||
key: "leaseIssued",
|
fontWeight: 700,
|
||||||
label: tunnel?.tunnel_id
|
}}
|
||||||
? `Tunnel ${tunnel.tunnel_id.slice(0, 8)} Created @ Port ${tunnel.port || "-"}`
|
>
|
||||||
: "Tunnel Created",
|
Tunnel:{" "}
|
||||||
},
|
<Typography component="span" variant="body2" sx={{ color: MAGIC_UI.textMuted, fontWeight: 500 }}>
|
||||||
{ key: "operatorJoined", label: "Attach Operator" },
|
{tunnelSteps.join(" > ")}
|
||||||
{ key: "channelOpened", label: "Open Remote Shell" },
|
</Typography>
|
||||||
{ key: "ack", label: "Shell Ready Acknowledged" },
|
</Typography>
|
||||||
{ key: "active", label: "Remote Shell Session Active" },
|
<Typography
|
||||||
].map((step, idx, arr) => {
|
variant="body2"
|
||||||
const firstPendingIdx = arr.findIndex((s) => !milestones[s.key]);
|
sx={{
|
||||||
const flowActiveStates = ["requesting", "waiting", "waiting_agent", "lease_issued", "connected"];
|
color: milestones.operatorAttached ? MAGIC_UI.accentC : MAGIC_UI.textMuted,
|
||||||
const allowActive = flowActiveStates.includes(sessionState);
|
fontWeight: 700,
|
||||||
const status = milestones[step.key]
|
}}
|
||||||
? "done"
|
>
|
||||||
: idx === firstPendingIdx && allowActive
|
Websocket:{" "}
|
||||||
? "active"
|
<Typography component="span" variant="body2" sx={{ color: MAGIC_UI.textMuted, fontWeight: 500 }}>
|
||||||
: "idle";
|
{websocketSteps.join(" > ")}
|
||||||
const palette =
|
</Typography>
|
||||||
status === "done"
|
</Typography>
|
||||||
? { bg: "rgba(52,211,153,0.15)", border: "#34d399", color: "#34d399" }
|
<Typography
|
||||||
: status === "active"
|
variant="body2"
|
||||||
? { bg: "rgba(125,211,252,0.18)", border: MAGIC_UI.accentA, color: MAGIC_UI.accentA }
|
sx={{
|
||||||
: { bg: "rgba(148,163,184,0.12)", border: `${MAGIC_UI.panelBorder}`, color: MAGIC_UI.textMuted };
|
color: milestones.shellEstablished ? MAGIC_UI.accentC : MAGIC_UI.textMuted,
|
||||||
return (
|
fontWeight: 700,
|
||||||
<React.Fragment key={step.key}>
|
}}
|
||||||
<Chip
|
>
|
||||||
label={step.label}
|
Remote Shell:{" "}
|
||||||
icon={status === "done" ? <ActivityIcon sx={{ fontSize: 16 }} /> : <PlayIcon sx={{ fontSize: 16 }} />}
|
<Typography component="span" variant="body2" sx={{ color: MAGIC_UI.textMuted, fontWeight: 500 }}>
|
||||||
sx={{
|
{shellSteps.join(" > ")}
|
||||||
backgroundColor: palette.bg,
|
</Typography>
|
||||||
color: palette.color,
|
</Typography>
|
||||||
border: `1px solid ${palette.border}`,
|
</Stack>
|
||||||
fontWeight: 600,
|
|
||||||
".MuiChip-icon": { color: palette.color },
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{idx < arr.length - 1 ? (
|
|
||||||
<Typography variant="body2" sx={{ color: MAGIC_UI.textMuted, px: 0.25, fontWeight: 700 }}>
|
|
||||||
→
|
|
||||||
</Typography>
|
|
||||||
) : null}
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Box>
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
sx={{
|
|
||||||
mt: 0.5,
|
|
||||||
color: MAGIC_UI.textMuted,
|
|
||||||
fontWeight: 600,
|
|
||||||
textOverflow: "ellipsis",
|
|
||||||
overflow: "hidden",
|
|
||||||
whiteSpace: "nowrap",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{statusLine}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
|
|||||||
Reference in New Issue
Block a user