testeranto
Version:
the AI powered BDD test framework for typescript projects
201 lines (200 loc) • 13.1 kB
JavaScript
/* eslint-disable @typescript-eslint/no-unused-vars */
import React, { useState, useEffect, useRef } from "react";
import { Container, Row, Col, Badge, Button } from "react-bootstrap";
import { useNavigate } from "react-router-dom";
export const ProcessManagerView = ({ processes, onRefresh, onBack, loading, onKillProcess, }) => {
const navigate = useNavigate();
const [selectedProcess, setSelectedProcess] = useState(null);
const [ws, setWs] = useState(null);
const [processLogs, setProcessLogs] = useState([]);
const [autoScroll, setAutoScroll] = useState(true);
const logsContainerRef = useRef(null);
const [isNavbarCollapsed, setIsNavbarCollapsed] = useState(false);
// Set up WebSocket connection
useEffect(() => {
const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const wsUrl = `${wsProtocol}//${window.location.host}`;
const websocket = new WebSocket(wsUrl);
setWs(websocket);
websocket.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
// Handle process data response
if (data.type === "processData" &&
selectedProcess &&
data.processId === selectedProcess.processId) {
setSelectedProcess((prev) => prev
? Object.assign(Object.assign({}, prev), { logs: data.logs || [] }) : null);
setProcessLogs(data.logs || []);
}
// Handle new log messages
else if ((data.type === "processStdout" || data.type === "processStderr") &&
selectedProcess &&
data.processId === selectedProcess.processId) {
setProcessLogs((prev) => [...prev, data.data]);
}
}
catch (error) {
console.error("Error parsing WebSocket message:", error);
}
};
return () => {
websocket.close();
};
}, [selectedProcess]);
// Request process data when selected
useEffect(() => {
if (selectedProcess && ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: "getProcess",
processId: selectedProcess.processId,
}));
}
}, [selectedProcess, ws]);
// Auto-scroll to bottom when new logs arrive and autoScroll is enabled
useEffect(() => {
if (autoScroll && logsContainerRef.current) {
logsContainerRef.current.scrollTop =
logsContainerRef.current.scrollHeight;
}
}, [processLogs, autoScroll]);
// Handle scroll events to determine if we should auto-scroll
const handleLogsScroll = () => {
if (logsContainerRef.current) {
const { scrollTop, scrollHeight, clientHeight } = logsContainerRef.current;
const isAtBottom = scrollHeight - scrollTop <= clientHeight + 10; // 10px threshold
setAutoScroll(isAtBottom);
}
};
const getStatusBadge = (process) => {
switch (process.status) {
case "running":
return React.createElement(Badge, { bg: "success" }, "Running");
case "exited":
return React.createElement(Badge, { bg: "secondary" },
"Exited (",
process.exitCode,
")");
case "error":
return React.createElement(Badge, { bg: "danger" }, "Error");
default:
return React.createElement(Badge, { bg: "warning" }, "Unknown");
}
};
const handleSelectProcess = (process) => {
setSelectedProcess(process);
setProcessLogs(process.logs || []);
};
const handleInput = (data) => {
if (ws &&
ws.readyState === WebSocket.OPEN &&
(selectedProcess === null || selectedProcess === void 0 ? void 0 : selectedProcess.status) === "running") {
ws.send(JSON.stringify({
type: "stdin",
processId: selectedProcess.processId,
data: data,
}));
}
};
return (React.createElement(Container, { fluid: true, className: "px-0 h-100" },
React.createElement(Row, { className: "g-0", style: { height: "calc(100vh - 56px)" } },
React.createElement(Col, { sm: 3, className: "border-end", style: {
height: "100%",
overflow: "auto",
backgroundColor: "#f8f9fa",
} },
React.createElement("div", { className: "p-1" },
processes.map((process) => (React.createElement("div", { key: process.processId, className: `p-2 mb-1 rounded ${(selectedProcess === null || selectedProcess === void 0 ? void 0 : selectedProcess.processId) === process.processId
? "bg-primary text-white"
: "bg-white border"}`, style: { cursor: "pointer" }, onClick: () => handleSelectProcess(process), title: process.command },
React.createElement("div", { className: "d-flex justify-content-between align-items-start" },
React.createElement("div", { className: "flex-grow-1", style: { minWidth: 0 } },
React.createElement("div", { className: "fw-bold text-truncate small" }, process.command.split(" ")[0]),
React.createElement("div", { className: `text-truncate ${(selectedProcess === null || selectedProcess === void 0 ? void 0 : selectedProcess.processId) === process.processId
? "text-white-50"
: "text-muted"}`, style: { fontSize: "0.7rem" } },
"PID: ",
process.pid || "N/A",
" |",
" ",
new Date(process.timestamp).toLocaleTimeString())),
React.createElement("div", { className: "ms-2" }, getStatusBadge(process))),
process.error && (React.createElement("div", { className: `mt-1 ${(selectedProcess === null || selectedProcess === void 0 ? void 0 : selectedProcess.processId) === process.processId
? "text-warning"
: "text-danger"}`, style: { fontSize: "0.7rem" } },
React.createElement("div", { className: "text-truncate" },
"Error: ",
process.error)))))),
processes.length === 0 && !loading && (React.createElement("div", { className: "p-2 text-center text-muted small" }, "No active processes")),
loading && processes.length === 0 && (React.createElement("div", { className: "p-2 text-center small" },
React.createElement("div", { className: "spinner-border spinner-border-sm", role: "status" },
React.createElement("span", { className: "visually-hidden" }, "Loading...")))))),
React.createElement(Col, { sm: 5, className: "border-end p-3", style: { height: "100%", overflow: "auto" } }, selectedProcess ? (React.createElement("div", null,
React.createElement("div", { className: "mb-3" },
React.createElement("strong", null, "Command:"),
React.createElement("code", { className: "bg-light p-2 rounded d-block mt-1", style: { fontSize: "0.8rem" } }, selectedProcess.command)),
React.createElement("div", { className: "mb-2" },
React.createElement("strong", null, "Status:"),
React.createElement("div", { className: "mt-1" }, getStatusBadge(selectedProcess))),
React.createElement("div", { className: "mb-2" },
React.createElement("strong", null, "PID:"),
React.createElement("div", { className: "text-muted" }, selectedProcess.pid || "N/A")),
React.createElement("div", { className: "mb-2" },
React.createElement("strong", null, "Started:"),
React.createElement("div", { className: "text-muted" }, new Date(selectedProcess.timestamp).toLocaleString())),
selectedProcess.exitCode !== undefined && (React.createElement("div", { className: "mb-2" },
React.createElement("strong", null, "Exit Code:"),
React.createElement("div", { className: "text-muted" }, selectedProcess.exitCode))),
selectedProcess.error && (React.createElement("div", { className: "mt-3" },
React.createElement("strong", { className: "text-danger" }, "Error:"),
React.createElement("div", { className: "text-danger small mt-1" }, selectedProcess.error))))) : (React.createElement("div", { className: "text-center text-muted mt-5" },
React.createElement("i", null, "Select a process to view details")))),
React.createElement(Col, { sm: 4, className: "p-0", style: { height: "100%", overflow: "hidden" } }, selectedProcess ? (React.createElement("div", { className: "d-flex flex-column h-100" },
React.createElement("div", { ref: logsContainerRef, className: "bg-dark text-light flex-grow-1", style: {
overflowY: "auto",
fontFamily: 'Monaco, Menlo, "Ubuntu Mono", monospace',
fontSize: "14px",
lineHeight: "1.4",
// padding: '1rem'
}, onScroll: handleLogsScroll },
processLogs.length > 0 ? (React.createElement("pre", { className: "mb-0", style: {
whiteSpace: "pre-wrap",
wordBreak: "break-word",
backgroundColor: "transparent",
border: "none",
color: "inherit",
} }, processLogs.join(""))) : (React.createElement("div", { className: "text-muted text-center py-4" },
React.createElement("i", null, "No output yet"))),
!autoScroll && (React.createElement("div", { className: "position-sticky bottom-0 d-flex justify-content-center mb-2" },
React.createElement(Button, { variant: "primary", size: "sm", onClick: () => {
setAutoScroll(true);
if (logsContainerRef.current) {
logsContainerRef.current.scrollTop =
logsContainerRef.current.scrollHeight;
}
} }, "Scroll to Bottom")))),
selectedProcess.status === "running" && (React.createElement("div", { className: "border-top bg-white p-3", style: { flexShrink: 0 } },
onKillProcess && (React.createElement("div", { className: "mb-3" },
React.createElement(Button, { variant: "danger", size: "sm", onClick: () => onKillProcess(selectedProcess.processId), className: "w-100" }, "\u23F9\uFE0F Stop Process"))),
React.createElement("div", { className: "input-group" },
React.createElement("input", { type: "text", className: "form-control", placeholder: "Type input and press Enter...", onKeyPress: (e) => {
if (e.key === "Enter") {
const target = e.target;
const inputValue = target.value;
if (inputValue.trim()) {
handleInput(inputValue + "\n");
target.value = "";
}
}
}, autoFocus: true }),
React.createElement("button", { className: "btn btn-primary", type: "button", onClick: () => {
const input = document.querySelector("input");
const inputValue = input.value;
if (inputValue.trim()) {
handleInput(inputValue + "\n");
input.value = "";
}
} }, "Send")),
React.createElement("small", { className: "text-muted" }, "\uD83D\uDCA1 Press Enter to send input to the process"))))) : (React.createElement("div", { className: "p-3 text-center text-muted mt-5" },
React.createElement("i", null, "Live logs will appear here when a process is selected")))))));
};