mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-12-15 16:55:48 -07:00
Updated Design of Status Pills on Remote Shell
This commit is contained in:
@@ -27,6 +27,16 @@ import "prismjs/components/prism-powershell";
|
|||||||
import "prismjs/themes/prism-okaidia.css";
|
import "prismjs/themes/prism-okaidia.css";
|
||||||
import Editor from "react-simple-code-editor";
|
import Editor from "react-simple-code-editor";
|
||||||
|
|
||||||
|
// Console diagnostics for troubleshooting the connect/disconnect flow.
|
||||||
|
const debugLog = (...args) => {
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error("[ReverseTunnel][PS]", ...args);
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const MAGIC_UI = {
|
const MAGIC_UI = {
|
||||||
panelBg: "rgba(7,11,24,0.92)",
|
panelBg: "rgba(7,11,24,0.92)",
|
||||||
panelBorder: "rgba(148, 163, 184, 0.35)",
|
panelBorder: "rgba(148, 163, 184, 0.35)",
|
||||||
@@ -108,6 +118,14 @@ 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 [milestones, setMilestones] = useState({
|
||||||
|
requested: false,
|
||||||
|
leaseIssued: false,
|
||||||
|
operatorJoined: false,
|
||||||
|
channelOpened: false,
|
||||||
|
ack: false,
|
||||||
|
active: false,
|
||||||
|
});
|
||||||
const socketRef = useRef(null);
|
const socketRef = useRef(null);
|
||||||
const pollTimerRef = useRef(null);
|
const pollTimerRef = useRef(null);
|
||||||
const resizeTimerRef = useRef(null);
|
const resizeTimerRef = useRef(null);
|
||||||
@@ -116,6 +134,18 @@ export default function ReverseTunnelPowershell({ device }) {
|
|||||||
const joinAttemptsRef = useRef(0);
|
const joinAttemptsRef = useRef(0);
|
||||||
const DOMAIN_REMOTE_SHELL = "remote-interactive-shell";
|
const DOMAIN_REMOTE_SHELL = "remote-interactive-shell";
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
debugLog("component mount", { hostname: device?.hostname, agentId });
|
||||||
|
return () => debugLog("component unmount");
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
debugLog("component mount", { hostname: device?.hostname, agentId });
|
||||||
|
return () => debugLog("component unmount");
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
const hostname = useMemo(() => {
|
const hostname = useMemo(() => {
|
||||||
return (
|
return (
|
||||||
normalizeText(device?.hostname) ||
|
normalizeText(device?.hostname) ||
|
||||||
@@ -139,6 +169,7 @@ export default function ReverseTunnelPowershell({ device }) {
|
|||||||
}, [device]);
|
}, [device]);
|
||||||
|
|
||||||
const resetState = useCallback(() => {
|
const resetState = useCallback(() => {
|
||||||
|
debugLog("resetState invoked");
|
||||||
setTunnel(null);
|
setTunnel(null);
|
||||||
setSessionState("idle");
|
setSessionState("idle");
|
||||||
setStatusMessage("");
|
setStatusMessage("");
|
||||||
@@ -146,6 +177,14 @@ export default function ReverseTunnelPowershell({ device }) {
|
|||||||
setOutput("");
|
setOutput("");
|
||||||
setInput("");
|
setInput("");
|
||||||
setPsStatus({});
|
setPsStatus({});
|
||||||
|
setMilestones({
|
||||||
|
requested: false,
|
||||||
|
leaseIssued: false,
|
||||||
|
operatorJoined: false,
|
||||||
|
channelOpened: false,
|
||||||
|
ack: false,
|
||||||
|
active: false,
|
||||||
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const disconnectSocket = useCallback(() => {
|
const disconnectSocket = useCallback(() => {
|
||||||
@@ -184,6 +223,7 @@ export default function ReverseTunnelPowershell({ device }) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
|
debugLog("cleanup on unmount", { tunnelId: tunnel?.tunnel_id });
|
||||||
stopPolling();
|
stopPolling();
|
||||||
disconnectSocket();
|
disconnectSocket();
|
||||||
if (joinRetryRef.current) {
|
if (joinRetryRef.current) {
|
||||||
@@ -257,10 +297,12 @@ export default function ReverseTunnelPowershell({ device }) {
|
|||||||
const pollLoop = useCallback(
|
const pollLoop = useCallback(
|
||||||
(socket, tunnelId) => {
|
(socket, tunnelId) => {
|
||||||
if (!socket || !tunnelId) return;
|
if (!socket || !tunnelId) return;
|
||||||
|
debugLog("pollLoop tick", { tunnelId });
|
||||||
setPolling(true);
|
setPolling(true);
|
||||||
pollTimerRef.current = setTimeout(async () => {
|
pollTimerRef.current = setTimeout(async () => {
|
||||||
const resp = await emitAsync(socket, "ps_poll", {});
|
const resp = await emitAsync(socket, "ps_poll", {});
|
||||||
if (resp?.error) {
|
if (resp?.error) {
|
||||||
|
debugLog("pollLoop error", resp);
|
||||||
stopPolling();
|
stopPolling();
|
||||||
disconnectSocket();
|
disconnectSocket();
|
||||||
setPsStatus({});
|
setPsStatus({});
|
||||||
@@ -273,6 +315,7 @@ export default function ReverseTunnelPowershell({ device }) {
|
|||||||
}
|
}
|
||||||
if (resp?.status) {
|
if (resp?.status) {
|
||||||
setPsStatus(resp.status);
|
setPsStatus(resp.status);
|
||||||
|
debugLog("pollLoop status", resp.status);
|
||||||
if (resp.status.closed) {
|
if (resp.status.closed) {
|
||||||
setSessionState("closed");
|
setSessionState("closed");
|
||||||
setTunnel(null);
|
setTunnel(null);
|
||||||
@@ -281,6 +324,7 @@ export default function ReverseTunnelPowershell({ device }) {
|
|||||||
}
|
}
|
||||||
if (resp.status.ack) {
|
if (resp.status.ack) {
|
||||||
setSessionState("connected");
|
setSessionState("connected");
|
||||||
|
setMilestones((prev) => ({ ...prev, ack: true, active: true }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pollLoop(socket, tunnelId);
|
pollLoop(socket, tunnelId);
|
||||||
@@ -291,6 +335,7 @@ export default function ReverseTunnelPowershell({ device }) {
|
|||||||
|
|
||||||
const handleDisconnect = useCallback(
|
const handleDisconnect = useCallback(
|
||||||
async (reason = "operator_disconnect") => {
|
async (reason = "operator_disconnect") => {
|
||||||
|
debugLog("handleDisconnect begin", { reason, tunnelId: tunnel?.tunnel_id, psStatus, sessionState });
|
||||||
const socket = socketRef.current;
|
const socket = socketRef.current;
|
||||||
const tunnelId = tunnel?.tunnel_id;
|
const tunnelId = tunnel?.tunnel_id;
|
||||||
if (joinRetryRef.current) {
|
if (joinRetryRef.current) {
|
||||||
@@ -300,13 +345,17 @@ export default function ReverseTunnelPowershell({ device }) {
|
|||||||
joinAttemptsRef.current = 0;
|
joinAttemptsRef.current = 0;
|
||||||
if (socket && tunnelId) {
|
if (socket && tunnelId) {
|
||||||
const frame = buildCloseFrame(1, CLOSE_AGENT_SHUTDOWN, "operator_close");
|
const frame = buildCloseFrame(1, CLOSE_AGENT_SHUTDOWN, "operator_close");
|
||||||
|
debugLog("emit CLOSE", { tunnelId });
|
||||||
socket.emit("send", { frame });
|
socket.emit("send", { frame });
|
||||||
}
|
}
|
||||||
await stopTunnel(reason);
|
await stopTunnel(reason);
|
||||||
|
debugLog("stopTunnel issued", { tunnelId });
|
||||||
stopPolling();
|
stopPolling();
|
||||||
disconnectSocket();
|
disconnectSocket();
|
||||||
setTunnel(null);
|
setTunnel(null);
|
||||||
setSessionState("closed");
|
setSessionState("closed");
|
||||||
|
setMilestones((prev) => ({ ...prev, active: false }));
|
||||||
|
debugLog("handleDisconnect finished", { tunnelId });
|
||||||
},
|
},
|
||||||
[disconnectSocket, stopPolling, stopTunnel, tunnel?.tunnel_id]
|
[disconnectSocket, stopPolling, stopTunnel, tunnel?.tunnel_id]
|
||||||
);
|
);
|
||||||
@@ -352,6 +401,7 @@ export default function ReverseTunnelPowershell({ device }) {
|
|||||||
socketRef.current = socket;
|
socketRef.current = socket;
|
||||||
|
|
||||||
socket.on("connect_error", () => {
|
socket.on("connect_error", () => {
|
||||||
|
debugLog("socket connect_error");
|
||||||
setStatusSeverity("warning");
|
setStatusSeverity("warning");
|
||||||
setStatusMessage("Tunnel namespace unavailable.");
|
setStatusMessage("Tunnel namespace unavailable.");
|
||||||
setTunnel(null);
|
setTunnel(null);
|
||||||
@@ -359,6 +409,7 @@ export default function ReverseTunnelPowershell({ device }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
socket.on("disconnect", () => {
|
socket.on("disconnect", () => {
|
||||||
|
debugLog("socket disconnect", { tunnelId: tunnel?.tunnel_id });
|
||||||
stopPolling();
|
stopPolling();
|
||||||
if (sessionState !== "closed") {
|
if (sessionState !== "closed") {
|
||||||
setSessionState("disconnected");
|
setSessionState("disconnected");
|
||||||
@@ -369,6 +420,8 @@ export default function ReverseTunnelPowershell({ device }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
socket.on("connect", async () => {
|
socket.on("connect", async () => {
|
||||||
|
debugLog("socket connect", { tunnelId: lease.tunnel_id });
|
||||||
|
setMilestones((prev) => ({ ...prev, operatorJoined: true }));
|
||||||
setStatusSeverity("info");
|
setStatusSeverity("info");
|
||||||
setStatusMessage("Joining tunnel...");
|
setStatusMessage("Joining tunnel...");
|
||||||
const joinResp = await emitAsync(socket, "join", { tunnel_id: lease.tunnel_id });
|
const joinResp = await emitAsync(socket, "join", { tunnel_id: lease.tunnel_id });
|
||||||
@@ -388,6 +441,7 @@ export default function ReverseTunnelPowershell({ device }) {
|
|||||||
setStatusMessage("Agent did not attach to tunnel (timeout). Try Connect again.");
|
setStatusMessage("Agent did not attach to tunnel (timeout). Try Connect again.");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
debugLog("join error", joinResp);
|
||||||
setSessionState("error");
|
setSessionState("error");
|
||||||
setStatusSeverity("error");
|
setStatusSeverity("error");
|
||||||
setStatusMessage(joinResp.error);
|
setStatusMessage(joinResp.error);
|
||||||
@@ -395,6 +449,8 @@ export default function ReverseTunnelPowershell({ device }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const dims = measureTerminal();
|
const dims = measureTerminal();
|
||||||
|
debugLog("ps_open emit", { tunnelId: lease.tunnel_id, dims });
|
||||||
|
setMilestones((prev) => ({ ...prev, channelOpened: true }));
|
||||||
const openResp = await emitAsync(socket, "ps_open", dims);
|
const openResp = await emitAsync(socket, "ps_open", dims);
|
||||||
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.
|
||||||
@@ -415,6 +471,7 @@ export default function ReverseTunnelPowershell({ device }) {
|
|||||||
connectSocket(tunnel);
|
connectSocket(tunnel);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
debugLog("requestTunnel", { agentId, connectionType });
|
||||||
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.");
|
||||||
@@ -443,6 +500,7 @@ export default function ReverseTunnelPowershell({ device }) {
|
|||||||
setStatusMessage("");
|
setStatusMessage("");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
setMilestones((prev) => ({ ...prev, requested: true, leaseIssued: true }));
|
||||||
setTunnel(data);
|
setTunnel(data);
|
||||||
setStatusMessage("");
|
setStatusMessage("");
|
||||||
setSessionState("lease_issued");
|
setSessionState("lease_issued");
|
||||||
@@ -491,11 +549,6 @@ export default function ReverseTunnelPowershell({ device }) {
|
|||||||
}, [stopTunnel, tunnel?.tunnel_id]);
|
}, [stopTunnel, tunnel?.tunnel_id]);
|
||||||
|
|
||||||
const sessionChips = [
|
const sessionChips = [
|
||||||
{
|
|
||||||
label: isConnected ? "Connected" : isClosed ? "Session ended" : sessionState === "idle" ? "Idle" : sessionState.replace(/_/g, " "),
|
|
||||||
color: isConnected ? MAGIC_UI.accentC : isClosed ? MAGIC_UI.accentB : MAGIC_UI.accentA,
|
|
||||||
icon: <ActivityIcon sx={{ fontSize: 18 }} />,
|
|
||||||
},
|
|
||||||
tunnel?.tunnel_id
|
tunnel?.tunnel_id
|
||||||
? {
|
? {
|
||||||
label: `Tunnel ${tunnel.tunnel_id.slice(0, 8)}`,
|
label: `Tunnel ${tunnel.tunnel_id.slice(0, 8)}`,
|
||||||
@@ -534,28 +587,6 @@ export default function ReverseTunnelPowershell({ device }) {
|
|||||||
<Typography variant="h6" sx={{ fontWeight: 700, letterSpacing: 0.3 }}>
|
<Typography variant="h6" sx={{ fontWeight: 700, letterSpacing: 0.3 }}>
|
||||||
Remote Shell
|
Remote Shell
|
||||||
</Typography>
|
</Typography>
|
||||||
{hostname ? (
|
|
||||||
<Chip
|
|
||||||
size="small"
|
|
||||||
label={hostname}
|
|
||||||
sx={{
|
|
||||||
background: "rgba(12,18,35,0.8)",
|
|
||||||
color: MAGIC_UI.textBright,
|
|
||||||
border: `1px solid ${MAGIC_UI.panelBorder}`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
{agentId ? (
|
|
||||||
<Chip
|
|
||||||
size="small"
|
|
||||||
label={`Agent ${agentId}`}
|
|
||||||
sx={{
|
|
||||||
background: "rgba(12,18,35,0.8)",
|
|
||||||
color: MAGIC_UI.textMuted,
|
|
||||||
border: `1px solid ${MAGIC_UI.panelBorder}`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Stack direction={{ xs: "column", sm: "row" }} spacing={1} alignItems="center">
|
<Stack direction={{ xs: "column", sm: "row" }} spacing={1} alignItems="center">
|
||||||
@@ -594,21 +625,53 @@ export default function ReverseTunnelPowershell({ device }) {
|
|||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Stack direction={{ xs: "column", md: "row" }} spacing={1} sx={{ mt: 1, flexWrap: "wrap" }}>
|
{/* Workflow pills */}
|
||||||
{sessionChips.map((chip) => (
|
<Box sx={{ mt: 1.5, display: "flex", flexWrap: "wrap", alignItems: "center", gap: 0.75 }}>
|
||||||
<Chip
|
{[
|
||||||
key={chip.label}
|
{ key: "requested", label: "Request Tunnel" },
|
||||||
icon={chip.icon}
|
{
|
||||||
label={chip.label}
|
key: "leaseIssued",
|
||||||
sx={{
|
label: tunnel?.tunnel_id ? `Tunnel ${tunnel.tunnel_id.slice(0, 8)} @ Port ${tunnel.port || "-"}` : "Lease Issued",
|
||||||
background: "rgba(12,18,35,0.85)",
|
},
|
||||||
color: chip.color,
|
{ key: "operatorJoined", label: "Operator Joined" },
|
||||||
border: `1px solid ${chip.color}44`,
|
{ key: "channelOpened", label: "Open PS Channel" },
|
||||||
fontWeight: 600,
|
{ key: "ack", label: "Shell ACK" },
|
||||||
}}
|
{ key: "active", label: "Session Active" },
|
||||||
/>
|
].map((step, idx, arr) => {
|
||||||
))}
|
const firstPendingIdx = arr.findIndex((s) => !milestones[s.key]);
|
||||||
</Stack>
|
const status = milestones[step.key]
|
||||||
|
? "done"
|
||||||
|
: idx === firstPendingIdx && sessionState !== "idle"
|
||||||
|
? "active"
|
||||||
|
: "idle";
|
||||||
|
const palette =
|
||||||
|
status === "done"
|
||||||
|
? { bg: "rgba(52,211,153,0.15)", border: "#34d399", color: "#34d399" }
|
||||||
|
: status === "active"
|
||||||
|
? { bg: "rgba(125,211,252,0.18)", border: MAGIC_UI.accentA, color: MAGIC_UI.accentA }
|
||||||
|
: { bg: "rgba(148,163,184,0.12)", border: `${MAGIC_UI.panelBorder}`, color: MAGIC_UI.textMuted };
|
||||||
|
return (
|
||||||
|
<React.Fragment key={step.key}>
|
||||||
|
<Chip
|
||||||
|
label={step.label}
|
||||||
|
icon={status === "done" ? <ActivityIcon sx={{ fontSize: 16 }} /> : <PlayIcon sx={{ fontSize: 16 }} />}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: palette.bg,
|
||||||
|
color: palette.color,
|
||||||
|
border: `1px solid ${palette.border}`,
|
||||||
|
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>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
|
|||||||
Reference in New Issue
Block a user