This commit is contained in:
Benjamin Toby 2026-03-10 11:45:06 +00:00
parent 921375c53d
commit f3b42bc206
11 changed files with 243 additions and 49 deletions

View File

@ -49,15 +49,15 @@ export default function TtydIframe({
<Span size="small" variant="faded">
{title}
</Span>
<LucideIcon
{/* <LucideIcon
name="ChevronRight"
size={15}
opacity={0.3}
/>
/> */}
</Fragment>
) : null}
<Span size="small" variant="faded">
{/* <Span size="small" variant="faded">
<a
href={url}
target="_blank"
@ -65,7 +65,7 @@ export default function TtydIframe({
>
{url}
</a>
</Span>
</Span> */}
</Row>
<Row>
<Button

View File

@ -0,0 +1,66 @@
import Stack from "@/twui/components/layout/Stack";
import {
Dispatch,
SetStateAction,
useContext,
useEffect,
useRef,
useState,
} from "react";
import { AppContext } from "@/src/pages/_app";
import {
NormalizedServerObject,
ParsedDeploymentServiceConfig,
} from "@/src/types";
import Select, { TWUISelectOptionObject } from "@/twui/components/form/Select";
type Props = {
service: ParsedDeploymentServiceConfig;
server: NormalizedServerObject;
setLog: Dispatch<SetStateAction<string | undefined>>;
};
export default function ServiceClusterServerLogSelector({
service,
server,
setLog: externalSetLog,
}: Props) {
const { pageProps } = useContext(AppContext);
const logs = service.logs;
const log_strings = logs?.map((l) => (typeof l == "string" ? l : l.cmd));
const [log, setLog] = useState(log_strings?.[0]);
const [isCustomLog, setIsCustomLog] = useState(false);
useEffect(() => {
externalSetLog(log);
}, [log]);
return (
<Stack className="w-full gap-0">
<Select
options={[
...(log_strings?.map(
(l) =>
({ value: l, title: l }) as TWUISelectOptionObject,
) || []),
{
value: "custom",
title: "--Custom--",
},
]}
changeHandler={(v) => {
if (v == "custom") {
setIsCustomLog(true);
} else {
setIsCustomLog(false);
setLog(v);
}
}}
/>
{isCustomLog}
</Stack>
);
}

View File

@ -4,6 +4,7 @@ import { AppContext } from "@/src/pages/_app";
import {
NormalizedServerObject,
ParsedDeploymentServiceConfig,
ServerTerminalTargets,
TtydInfoObject,
WebSocketDataType,
} from "@/src/types";
@ -13,13 +14,21 @@ import Loading from "@/twui/components/elements/Loading";
import Center from "@/twui/components/layout/Center";
import TtydIframe from "@/src/components/general/ttyd-iframe";
import useIntersectionObserver from "@/twui/components/hooks/useIntersectionObserver";
import useStatus from "@/twui/components/hooks/useStatus";
type Props = {
service: ParsedDeploymentServiceConfig;
server: NormalizedServerObject;
target: (typeof ServerTerminalTargets)[number]["name"];
log?: string;
};
export default function ServiceClusterServerViews({ service, server }: Props) {
export default function ServiceClusterServerViews({
service,
server,
target,
log,
}: Props) {
const { pageProps, ws } = useContext(AppContext);
const viewRef = useRef<HTMLDivElement>(undefined);
@ -27,26 +36,62 @@ export default function ServiceClusterServerViews({ service, server }: Props) {
const { data } = useWebSocketEventHandler<WebSocketDataType>();
const { isIntersecting } = useIntersectionObserver({ elementRef: viewRef });
const [ttydLogs, setTtydLogs] = useState<TtydInfoObject>();
const [ttyd, setTtyd] = useState<TtydInfoObject>();
const { refresh, setRefresh } = useStatus();
const WsReqSentRef = useRef(false);
function sendKillPort() {
if (ttyd?.port) {
ws.sendData({
event: "client:kill-port",
server,
service: _.omit(service, ["servers"]),
port: ttyd.port,
log,
});
}
}
useEffect(() => {
if (!ws?.socket || WsReqSentRef.current) {
return;
}
ws.sendData({
event: "client:service-server-logs",
server,
service: _.omit(service, ["servers"]),
});
if (target == "logs") {
ws.sendData({
event: "client:service-server-logs",
server,
service: _.omit(service, ["servers"]),
});
} else {
ws.sendData({
event: "client:service-server-shell",
server,
service: _.omit(service, ["servers"]),
});
}
WsReqSentRef.current = true;
}, [ws]);
return function () {
sendKillPort();
};
}, [ws, refresh]);
useEffect(() => {
if (ttydLogs) return;
if (WsReqSentRef.current) {
sendKillPort();
setTtyd(undefined);
WsReqSentRef.current = false;
setRefresh((prev) => prev + 1);
}
}, [target]);
useEffect(() => {
if (ttyd) return;
if (
data?.event == "server:service-server-logs" &&
@ -54,53 +99,45 @@ export default function ServiceClusterServerViews({ service, server }: Props) {
data.server?.private_ip == server.private_ip
) {
setTimeout(() => {
setTtydLogs(data.ttyd);
setTtyd(data.ttyd);
}, 2000);
}
if (
data?.event == "server:service-server-shell" &&
data?.ttyd &&
data.server?.private_ip == server.private_ip
) {
setTimeout(() => {
setTtyd(data.ttyd);
}, 2000);
}
}, [data]);
console.log("isIntersecting", isIntersecting);
console.log("ttydLogs", ttydLogs);
useEffect(() => {
if (!ttydLogs?.port) return;
if (!ttyd?.port) return;
if (!isIntersecting) {
ws.sendData({
event: "client:kill-port",
server,
service: _.omit(service, ["servers"]),
port: ttydLogs.port,
});
sendKillPort();
}
}, [isIntersecting]);
const dev_logs_url = ttydLogs?.port
? `http://localhost:${ttydLogs.port}`
: undefined;
// const dev_logs_url = ttydLogs?.port
// ? `http://localhost:${ttydLogs.port}`
// : undefined;
const title = (
<>
{service.service_name} service <code>{server.private_ip}</code> Logs
<code>{server.private_ip}</code> {target}
</>
);
return (
<Stack className="gap-0 w-full" componentRef={viewRef as any}>
{isIntersecting && ttydLogs?.url && ttydLogs.port ? (
{isIntersecting && ttyd?.url && ttyd.port ? (
<Stack className="gap-0">
{/* {dev_logs_url ? (
<TtydIframe
url={dev_logs_url}
title={title}
wrapperProps={{
className: "border-none",
}}
/>
) : null}
<hr /> */}
<TtydIframe
url={ttydLogs?.url}
url={ttyd?.url}
title={title}
wrapperProps={{
className: "border-none",
@ -108,7 +145,7 @@ export default function ServiceClusterServerViews({ service, server }: Props) {
/>
</Stack>
) : (
<Center className="p-10 h-[400px]">
<Center className="p-10 h-[460px]">
<Loading />
</Center>
)}

View File

@ -1,14 +1,18 @@
import Stack from "@/twui/components/layout/Stack";
import { useContext, useRef } from "react";
import { useContext, useRef, useState } from "react";
import { AppContext } from "@/src/pages/_app";
import {
NormalizedServerObject,
ParsedDeploymentServiceConfig,
ServerTerminalTargets,
} from "@/src/types";
import useIntersectionObserver from "@/twui/components/hooks/useIntersectionObserver";
import Center from "@/twui/components/layout/Center";
import Loading from "@/twui/components/elements/Loading";
import ServiceClusterServerViews from "./cluster-server-views";
import Row from "@/twui/components/layout/Row";
import Button from "@/twui/components/layout/Button";
import ServiceClusterServerLogSelector from "./cluster-server-log-selector";
type Props = {
service: ParsedDeploymentServiceConfig;
@ -21,14 +25,47 @@ export default function ServiceClusterServer({ service, server }: Props) {
const elementRef = useRef<HTMLDivElement>(undefined);
const { isIntersecting } = useIntersectionObserver({ elementRef });
const [target, setTarget] =
useState<(typeof ServerTerminalTargets)[number]["name"]>("logs");
const [log, setLog] = useState<string>();
return (
<Stack className="grid-cell gap-0" componentRef={elementRef as any}>
<Stack className="grid-cell-content">
<code>{server.private_ip}</code>
<Row className="w-full justify-between">
<Row>
<code>{server.private_ip}</code>
</Row>
<Row>
{ServerTerminalTargets.map((targ, index) => {
const is_active = targ.name == target;
return (
<Button
title={`${targ.name}`}
onClick={() => {
setTarget(targ.name);
}}
size="smaller"
color="gray"
variant={is_active ? undefined : "outlined"}
>
{targ.name}
</Button>
);
})}
</Row>
</Row>
<ServiceClusterServerLogSelector
{...{ server, service, setLog }}
/>
</Stack>
<hr />
{isIntersecting ? (
<ServiceClusterServerViews {...{ server, service }} />
<ServiceClusterServerViews
{...{ server, service, target, log }}
/>
) : (
<Center>
<Loading />

View File

@ -50,7 +50,7 @@ export default async function grabTtydServerInfo({
cmd += ` root@${server?.private_ip}`;
if (final_first_log) {
if (paradigm == "logs" && final_first_log) {
cmd += ` ${final_first_log}`;
}

View File

@ -232,6 +232,7 @@ export const WebSocketEvents = [
"client:ping",
"client:service-server-logs",
"client:kill-port",
"client:service-server-shell",
"server:ping",
"server:error",
@ -241,6 +242,7 @@ export const WebSocketEvents = [
"server:update",
"server:service-server-logs",
"server:killed-port",
"server:service-server-shell",
] as const;
export type WebSocketDataType = {
@ -250,6 +252,7 @@ export type WebSocketDataType = {
server?: NormalizedServerObject;
ttyd?: TtydInfoObject;
port?: string | number;
log?: string;
};
export type WebSocketMessageParam = {
@ -291,3 +294,8 @@ export type WebSocketConnectedUserData = {
child_processes: ChildProcess[];
ports: (string | number)[];
};
export const ServerTerminalTargets = [
{ name: "logs" },
{ name: "shell" },
] as const;

View File

@ -6,7 +6,7 @@ type Params = {
};
export default async function killPort({ port, exec_options }: Params) {
const kill_ports_cmd = `ss -tlnp | awk -v p=${port} 'NR>1 {split($4,a,":"); if(a[length(a)]==p) print $6}' | grep -oP 'pid=\K[0-9]+' | xargs -r kill -9`;
const kill_ports_cmd = `ss -tlnp | awk -v p=${port} 'NR>1 {split($4,a,":"); if(a[length(a)]==p) print $6}' | grep -oP 'pid=\\K[0-9]+' | xargs -r kill -9`;
execSync(kill_ports_cmd, {
...exec_options,

View File

@ -2,6 +2,7 @@ import { WebSocketMessageParam } from "@/src/types";
import sendData from "../(utils)/send-data";
import sendError from "../(utils)/send-error";
import grabConnectedWebsocketUserdata from "../(utils)/grab-connected-websocket-user-data";
import killPort from "../(utils)/kill-port";
export default async function socketClientKillPort({
ws,
@ -15,7 +16,15 @@ export default async function socketClientKillPort({
const connected_user_data = grabConnectedWebsocketUserdata({ user });
console.log("connected_user_data", connected_user_data);
console.log("port", port);
if (port) {
await killPort({ port, exec_options: { stdio: "inherit" } });
connected_user_data.ports = connected_user_data.ports.filter(
(p) => p != port,
);
}
sendData(ws, {
event: "server:killed-port",

View File

@ -19,8 +19,6 @@ export default async function socketClientServiceServerLogs({
paradigm: "logs",
});
console.log("ttyd", ttyd);
sendData(ws, {
event: "server:service-server-logs",
ttyd,

View File

@ -0,0 +1,30 @@
import { WebSocketMessageParam } from "@/src/types";
import sendData from "../(utils)/send-data";
import sendError from "../(utils)/send-error";
import grabTtydServerInfo from "@/src/functions/ttyd/grab-ttyd-service-info";
export default async function socketClientServiceServerShell({
ws,
data,
}: WebSocketMessageParam) {
try {
const user = ws.data.user;
const service = data?.service;
const server = data?.server;
const ttyd = await grabTtydServerInfo({
server,
service,
user,
paradigm: "terminal",
});
sendData(ws, {
event: "server:service-server-shell",
ttyd,
server,
});
} catch (error: any) {
sendError(ws, "Service Server Logs Error! " + error.message);
}
}

View File

@ -9,6 +9,7 @@ import socketClientPing from "./events/client-ping";
import debugLog from "@moduletrace/datasquirel/dist/package-shared/utils/logging/debug-log";
import socketClientServiceServerLogs from "./events/client-service-server-logs";
import socketClientKillPort from "./events/client-kill-port";
import socketClientServiceServerShell from "./events/client-service-server-shell";
type Param = {
ws: ServerWebSocket<WebSocketData>;
@ -47,6 +48,14 @@ export default async function socketMessage({ ws, message }: Param) {
});
await socketClientServiceServerLogs(websocketMessageParams);
break;
case "client:service-server-shell":
debugLog({
log: `${userRef} Getting Service Server Shell ...`,
addTime: true,
label,
});
await socketClientServiceServerShell(websocketMessageParams);
break;
case "client:kill-port":
debugLog({
log: `${userRef} Killing Port ${data.port} ...`,