This commit is contained in:
Benjamin Toby 2026-03-10 14:50:08 +00:00
parent aeff7c6ff0
commit dcd7f022bd
10 changed files with 312 additions and 104 deletions

View File

@ -1,19 +1,73 @@
import { AppContext } from "@/src/pages/_app";
import { ParsedDeploymentServiceConfig } from "@/src/types";
import {
NormalizedServerObject,
ParsedDeploymentServiceConfig,
} from "@/src/types";
import ArrowedLink from "@/twui/components/layout/ArrowedLink";
import Button from "@/twui/components/layout/Button";
import H2 from "@/twui/components/layout/H2";
import Row from "@/twui/components/layout/Row";
import Stack from "@/twui/components/layout/Stack";
import { useContext } from "react";
import { useContext, useEffect, useRef, useState } from "react";
import ServiceClusterServer from "../service/(partials)/cluster-server";
import { twMerge } from "tailwind-merge";
import Select from "@/twui/components/form/Select";
import useStatus from "@/twui/components/hooks/useStatus";
import Loading from "@/twui/components/elements/Loading";
import _ from "lodash";
type Props = {
service: ParsedDeploymentServiceConfig;
};
export default function DeploymentService({ service }: Props) {
const { pageProps } = useContext(AppContext);
const { pageProps, ws } = useContext(AppContext);
const { deployment } = pageProps;
const { ready, setReady } = useStatus();
const children_services = deployment?.services.filter(
(srv) => srv.parent_service_name == service.service_name,
);
const all_clusters = [service, ...(children_services || [])];
const [targetCluster, setTargetCluster] = useState<
ParsedDeploymentServiceConfig | undefined
>(all_clusters?.[0]);
const targetClusterIndex =
all_clusters.findIndex(
(cl) => cl.service_name == targetCluster?.service_name,
) + 1;
const cluster_servers = targetCluster?.servers;
const [targetServer, setTargetServer] = useState<
NormalizedServerObject | undefined
>(cluster_servers?.[0]);
const portRef = useRef<number>(undefined);
useEffect(() => {
setReady(false);
console.log("portRef", portRef);
if (targetServer && portRef.current) {
ws.sendData({
event: "client:kill-port",
server: targetServer,
service: _.omit(service, ["servers"]),
port: portRef.current,
});
}
setTimeout(() => {
setReady(true);
}, 2000);
}, [targetCluster, targetServer]);
return (
<Stack className="grid-cell">
<Stack className="grid-cell-content">
@ -27,7 +81,75 @@ export default function DeploymentService({ service }: Props) {
/>
</Row>
<code>{service.service_name}</code>
<code>{service.service_name} service</code>
</Stack>
<hr />
<Stack className="gap-0">
<Row className="p-4 grid md:grid-cols-2">
<Select
options={all_clusters.map((cl) => ({
value: cl.service_name,
title: `Cluster #${targetClusterIndex}`,
}))}
changeHandler={(v) => {
setTargetCluster(
all_clusters.find((cl) => cl.service_name == v),
);
}}
/>
{cluster_servers ? (
<Select
options={cluster_servers.map((srv) => ({
value: srv.private_ip!,
title: srv.private_ip,
}))}
changeHandler={(v) => {
setTargetServer(
cluster_servers.find(
(srv) => srv.private_ip == v,
),
);
}}
/>
) : undefined}
{/* {cluster_servers?.map((server, index) => {
const is_active =
server?.private_ip == targetServer?.private_ip;
return (
<Button
title={`Cluster ${index + 1}`}
key={index}
color="gray"
variant={is_active ? undefined : "outlined"}
size="small"
onClick={() => {
setTargetServer(server);
}}
>
<span
className={twMerge(
is_active ? "font-semibold" : "",
)}
>
{server.private_ip}
</span>
</Button>
);
})} */}
</Row>
{ready ? (
targetServer ? (
<ServiceClusterServer
server={targetServer}
service={service}
portRef={portRef}
/>
) : undefined
) : undefined}
</Stack>
</Stack>
);

View File

@ -0,0 +1,65 @@
import { Dispatch, Fragment, SetStateAction, useContext, useRef } from "react";
import { ParsedDeploymentServiceConfig } from "@/src/types";
import Select, { TWUISelectOptionObject } from "@/twui/components/form/Select";
import Row from "@/twui/components/layout/Row";
import Button from "@/twui/components/layout/Button";
import Span from "@/twui/components/layout/Span";
import Border from "@/twui/components/elements/Border";
import { X } from "lucide-react";
type Props = {
service: ParsedDeploymentServiceConfig;
setLog: Dispatch<SetStateAction<string | undefined>>;
log?: string;
};
export default function ServiceClusterServerLogSelectorSelectLog({
service,
setLog,
log,
}: Props) {
const logs = service.logs;
const log_strings = logs?.map((l) => (typeof l == "string" ? l : l.cmd));
const is_custom_log =
log?.match(/./) && !Boolean(log_strings?.find((l) => l == log));
return (
<Fragment>
{is_custom_log ? (
<Row className="grow">
<Border className="w-full py-1 h-[42px]">
<Row className="w-full justify-between">
<Span>{log}</Span>
<Button
title="Clear Custom Log"
variant="ghost"
className="p-1"
onClick={() => {
setLog(undefined);
}}
>
<X size={17} />
</Button>
</Row>
</Border>
</Row>
) : (
<Select
options={[
...(log_strings?.map(
(l) =>
({
value: l,
title: l,
}) as TWUISelectOptionObject,
) || []),
]}
changeHandler={(v) => {
setLog(v);
}}
/>
)}
</Fragment>
);
}

View File

@ -0,0 +1,63 @@
import Stack from "@/twui/components/layout/Stack";
import { Dispatch, SetStateAction, useRef } from "react";
import Button from "@/twui/components/layout/Button";
import Modal from "@/twui/components/elements/Modal";
import H3 from "@/twui/components/layout/H3";
import Span from "@/twui/components/layout/Span";
import useStatus from "@/twui/components/hooks/useStatus";
import Textarea from "@/twui/components/form/Textarea";
import LucideIcon from "@/twui/components/elements/lucide-icon";
type Props = {
setLog: Dispatch<SetStateAction<string | undefined>>;
};
export default function ServiceClusterServerLogSelectorSetCustomLog({
setLog,
}: Props) {
const { open, setOpen } = useStatus();
const customLogRef = useRef("");
return (
<Modal
target={
<Button
title="Enter custom command"
size="small"
color="gray"
className="w-[42px] h-[42px]"
>
<LucideIcon name="Edit3" size={14} />
</Button>
}
setOpen={setOpen}
open={open}
>
<Stack>
<Stack className="gap-1">
<H3 className="admin-h3">Enter a custom Command</H3>
<Span variant="faded">
Enter a command to run a custom log
</Span>
</Stack>
<Textarea
placeholder="Enter custom command"
changeHandler={(v) => {
customLogRef.current = v;
}}
autoFocus
/>
<Button
title="Set Custom Command"
onClick={() => {
setLog(customLogRef.current.trim());
setOpen(false);
}}
>
Set Custom Command
</Button>
</Stack>
</Modal>
);
}

View File

@ -1,102 +1,34 @@
import Stack from "@/twui/components/layout/Stack";
import {
Dispatch,
SetStateAction,
useContext,
useEffect,
useRef,
useState,
} from "react";
import { AppContext } from "@/src/pages/_app";
import { Dispatch, SetStateAction } from "react";
import {
NormalizedServerObject,
ParsedDeploymentServiceConfig,
} from "@/src/types";
import Select, { TWUISelectOptionObject } from "@/twui/components/form/Select";
import AceEditor from "@/twui/components/editors/AceEditor";
import Row from "@/twui/components/layout/Row";
import Button from "@/twui/components/layout/Button";
import Modal from "@/twui/components/elements/Modal";
import H3 from "@/twui/components/layout/H3";
import Span from "@/twui/components/layout/Span";
import useStatus from "@/twui/components/hooks/useStatus";
import ServiceClusterServerLogSelectorSetCustomLog from "./cluster-server-log-selector-set-custom-log";
import ServiceClusterServerLogSelectorSelectLog from "./cluster-server-log-selector-select-log";
type Props = {
service: ParsedDeploymentServiceConfig;
server: NormalizedServerObject;
setLog: Dispatch<SetStateAction<string | undefined>>;
log?: string;
};
export default function ServiceClusterServerLogSelector({
service,
server,
setLog: externalSetLog,
setLog,
log,
}: 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);
const { open, setOpen } = useStatus();
const customLogRef = useRef("");
return (
<Stack className="w-full gap-2 p-4">
<Row className="flex-nowrap">
<Select
options={[
...(log_strings?.map(
(l) =>
({
value: l,
title: l,
}) as TWUISelectOptionObject,
) || []),
]}
changeHandler={(v) => {
if (v == "custom") {
setIsCustomLog(true);
} else {
setIsCustomLog(false);
setLog(v);
}
}}
<ServiceClusterServerLogSelectorSetCustomLog setLog={setLog} />
<ServiceClusterServerLogSelectorSelectLog
service={service}
setLog={setLog}
log={log}
/>
<Modal
target={
<Button title="Enter custom command" size="small">
Custom
</Button>
}
setOpen={setOpen}
open={open}
>
<Stack>
<Stack className="gap-1">
<H3 className="admin-h3">Enter a custom Command</H3>
<Span variant="faded">
Enter a command to run a custom log
</Span>
</Stack>
<AceEditor
placeholder="Enter custom command"
onChange={(v) => {
customLogRef.current = v;
}}
/>
<Button title="Set Custom Command" onClick={()=>{
}}>
Set Custom Command
</Button>
</Stack>
</Modal>
</Row>
</Stack>
);

View File

@ -20,14 +20,16 @@ type Props = {
service: ParsedDeploymentServiceConfig;
server: NormalizedServerObject;
target: (typeof ServerTerminalTargets)[number]["name"];
log?: string;
log_cmd?: string;
portRef?: RefObject<number | undefined>;
};
export default function ServiceClusterServerViews({
service,
server,
target,
log,
log_cmd,
portRef,
}: Props) {
const { pageProps, ws } = useContext(AppContext);
@ -49,7 +51,6 @@ export default function ServiceClusterServerViews({
server,
service: _.omit(service, ["servers"]),
port: ttyd.port,
log,
});
}
}
@ -64,6 +65,7 @@ export default function ServiceClusterServerViews({
event: "client:service-server-logs",
server,
service: _.omit(service, ["servers"]),
cmd: log_cmd,
});
} else {
ws.sendData({
@ -81,6 +83,8 @@ export default function ServiceClusterServerViews({
}, [ws, refresh]);
useEffect(() => {
console.log("log_cmd", log_cmd);
if (WsReqSentRef.current) {
sendKillPort();
@ -88,7 +92,7 @@ export default function ServiceClusterServerViews({
WsReqSentRef.current = false;
setRefresh((prev) => prev + 1);
}
}, [target]);
}, [target, log_cmd]);
useEffect(() => {
if (ttyd) return;
@ -112,6 +116,10 @@ export default function ServiceClusterServerViews({
setTtyd(data.ttyd);
}, 2000);
}
if (portRef && data?.ttyd?.port) {
portRef.current = data.ttyd.port;
}
}, [data]);
useEffect(() => {

View File

@ -1,5 +1,5 @@
import Stack from "@/twui/components/layout/Stack";
import { ComponentProps, useContext, useRef, useState } from "react";
import { ComponentProps, RefObject, useContext, useRef, useState } from "react";
import { AppContext } from "@/src/pages/_app";
import {
NormalizedServerObject,
@ -19,20 +19,23 @@ type Props = {
service: ParsedDeploymentServiceConfig;
server: NormalizedServerObject;
wrapperProps?: ComponentProps<typeof Stack>;
portRef?: RefObject<number | undefined>;
};
export default function ServiceClusterServer({
service,
server,
wrapperProps,
portRef,
}: Props) {
const { pageProps } = useContext(AppContext);
const elementRef = useRef<HTMLDivElement>(undefined);
const { isIntersecting } = useIntersectionObserver({ elementRef });
const [target, setTarget] =
useState<(typeof ServerTerminalTargets)[number]["name"]>("logs");
const [target, setTarget] = useState<
(typeof ServerTerminalTargets)[number]["name"]
>(service.logs?.[0] ? "logs" : "shell");
const [log, setLog] = useState<string>();
@ -67,9 +70,18 @@ export default function ServiceClusterServer({
})}
</Row>
</Row>
<ServiceClusterServerLogSelector {...{ server, service, setLog }} />
{target == "logs" ? (
<ServiceClusterServerLogSelector
{...{ server, service, setLog, log }}
/>
) : (
<Row className="h-[74px]"></Row>
)}
<hr />
<ServiceClusterServerViews {...{ server, service, target, log }} />
<ServiceClusterServerViews
{...{ server, service, target, portRef }}
log_cmd={log}
/>
{/* {isIntersecting ? (
<ServiceClusterServerViews
{...{ server, service, target, log }}

View File

@ -6,10 +6,7 @@ import {
ParsedDeploymentServiceConfig,
} from "@/src/types";
import Row from "@/twui/components/layout/Row";
import H2 from "@/twui/components/layout/H2";
import ServiceClusterServer from "./cluster-server";
import Button from "@/twui/components/layout/Button";
import { twMerge } from "tailwind-merge";
type Props = {
service: ParsedDeploymentServiceConfig;

View File

@ -20,6 +20,7 @@ type Params = {
service?: Omit<ParsedDeploymentServiceConfig, "servers">;
server?: NormalizedServerObject;
paradigm: (typeof PrivateServerTtydParadigms)[number]["name"];
cmd?: string;
};
export default async function grabTtydServerInfo({
@ -27,12 +28,15 @@ export default async function grabTtydServerInfo({
service,
user,
paradigm,
cmd: passed_log_cmd,
}: Params): Promise<TtydInfoObject> {
const first_log = service?.logs?.[0];
const final_first_log =
typeof first_log == "string" ? first_log : first_log?.cmd;
if (paradigm == "logs" && !final_first_log) {
const final_log = passed_log_cmd || final_first_log;
if (paradigm == "logs" && !final_log) {
throw new Error(`Service Doesn't have logs.`);
}
@ -42,16 +46,17 @@ export default async function grabTtydServerInfo({
let cmd = ``;
if (paradigm == "logs") {
cmd += ` ${grabSSHPrefix()}`;
} else {
cmd += ` ssh -i ${TURBOCI_SSH_KEY_FILE}`;
}
// if (paradigm == "logs") {
// } else {
// cmd += ` ssh -i ${TURBOCI_SSH_KEY_FILE}`;
// }
cmd += ` root@${server?.private_ip}`;
if (paradigm == "logs" && final_first_log) {
cmd += ` ${final_first_log}`;
if (paradigm == "logs" && final_log) {
cmd += ` ${final_log}`;
}
const ttyd_cmd = grabTtydCmd({

View File

@ -252,7 +252,7 @@ export type WebSocketDataType = {
server?: NormalizedServerObject;
ttyd?: TtydInfoObject;
port?: string | number;
log?: string;
cmd?: string;
};
export type WebSocketMessageParam = {

View File

@ -11,12 +11,16 @@ export default async function socketClientServiceServerLogs({
const user = ws.data.user;
const service = data?.service;
const server = data?.server;
const cmd = data?.cmd;
console.log("cmd", cmd);
const ttyd = await grabTtydServerInfo({
server,
service,
user,
paradigm: "logs",
cmd,
});
sendData(ws, {