From 5988906cffcfa5e4eaa0dfbfe6491f42b1c445f1 Mon Sep 17 00:00:00 2001 From: typescreep Date: Mon, 21 Jul 2025 20:26:49 +0300 Subject: [PATCH] pod terminal wip --- package-lock.json | 19 +- package.json | 3 + src/components/organisms/Factory/Factory.tsx | 2 + .../organisms/Factory/TestWs/TestWs.tsx | 169 ++++++++++++++++++ .../organisms/Factory/TestWs/index.ts | 1 + .../organisms/Factory/TestWs/styled.ts | 28 +++ 6 files changed, 221 insertions(+), 1 deletion(-) create mode 100644 src/components/organisms/Factory/TestWs/TestWs.tsx create mode 100644 src/components/organisms/Factory/TestWs/index.ts create mode 100644 src/components/organisms/Factory/TestWs/styled.ts diff --git a/package-lock.json b/package-lock.json index ac348a3..0b6050c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,11 +11,13 @@ "@ant-design/icons": "5.6.0", "@monaco-editor/react": "4.6.0", "@originjs/vite-plugin-federation": "1.3.6", - "@prorobotech/openapi-k8s-toolkit": "^0.0.1-alpha.62", + "@prorobotech/openapi-k8s-toolkit": "0.0.1-alpha.62", "@readme/openapi-parser": "4.0.0", "@reduxjs/toolkit": "2.2.5", "@tanstack/react-query": "5.62.2", "@tanstack/react-query-devtools": "5.62.2", + "@xterm/addon-fit": "^0.10.0", + "@xterm/xterm": "^5.5.0", "antd": "5.26.4", "axios": "1.4.0", "cross-env": "7.0.3", @@ -4436,6 +4438,21 @@ "vite": "^4 || ^5" } }, + "node_modules/@xterm/addon-fit": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz", + "integrity": "sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==", + "license": "MIT", + "peerDependencies": { + "@xterm/xterm": "^5.0.0" + } + }, + "node_modules/@xterm/xterm": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", + "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", + "license": "MIT" + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", diff --git a/package.json b/package.json index af31d68..f9fcec3 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,9 @@ "@reduxjs/toolkit": "2.2.5", "@tanstack/react-query": "5.62.2", "@tanstack/react-query-devtools": "5.62.2", + "@xterm/addon-attach": "0.11.0", + "@xterm/addon-fit": "0.10.0", + "@xterm/xterm": "5.5.0", "antd": "5.26.4", "axios": "1.4.0", "cross-env": "7.0.3", diff --git a/src/components/organisms/Factory/Factory.tsx b/src/components/organisms/Factory/Factory.tsx index 3a7f9db..b12ea34 100644 --- a/src/components/organisms/Factory/Factory.tsx +++ b/src/components/organisms/Factory/Factory.tsx @@ -12,6 +12,7 @@ import { useSelector } from 'react-redux' import { RootState } from 'store/store' import { BASE_API_GROUP, BASE_API_VERSION } from 'constants/customizationApiGroupAndVersion' import { HEAD_FIRST_ROW, HEAD_SECOND_ROW, FOOTER_HEIGHT, NAV_HEIGHT } from 'constants/blocksSizes' +import { TestWs } from './TestWs' type TFactoryProps = { setSidebarTags: (tags: string[]) => void @@ -59,6 +60,7 @@ export const Factory: FC = ({ setSidebarTags }) => { if (spec.withScrollableMainContentCard) { return ( + = ({ endpoint, namespace, podName }) => { + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState() + const [terminal, setTerminal] = useState() + const terminalRef = useRef(null) + const resizeObserverRef = useRef(null) + const fitAddon = new FitAddon() + + // const decoderRef = useRef(new TextDecoder('utf-8')) + + useEffect(() => { + const terminal = new XTerm({ + cursorBlink: false, + cursorStyle: 'block', + fontFamily: 'monospace', + fontSize: 10, + }) + terminal.loadAddon(fitAddon) + // terminal.loadAddon(new WebLinksAddon()) + setTerminal(terminal) + fitAddon.fit() + + return () => { + if (terminal) { + terminal.dispose() + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + useEffect(() => { + if (terminal) { + if (terminalRef.current) { + terminal.open(terminalRef.current) + setTerminal(terminal) + } + } + + // Initialize ResizeObserver to handle resizing + resizeObserverRef.current = new ResizeObserver(() => { + fitAddon.fit() + }) + + // Observe the terminal container for size changes + if (terminalRef.current) { + resizeObserverRef.current.observe(terminalRef.current) + } + + return () => { + // Clean up the ResizeObserver + if (resizeObserverRef.current) { + resizeObserverRef.current.disconnect() + } + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [terminal]) + + useEffect(() => { + if (!terminal) { + return + } + + const socket = new WebSocket(endpoint) + + socket.onopen = () => { + socket.send( + JSON.stringify({ + type: 'init', + payload: { namespace, podName }, + }), + ) + console.log('WebSocket Client Connected') + setIsLoading(false) + } + + socket.onmessage = event => { + const data = JSON.parse(event.data) + if (data.type === 'output') { + if (data.payload.type === 'Buffer' && Array.isArray(data.payload.data)) { + // Reconstruct bytes and decode to string + // const bytes = new Uint8Array(data.payload) + // const text = decoderRef.current.decode(bytes) + + const text = Buffer.from(data.payload.data) + console.log(text) + terminal.write(text.toString('utf8')) + } else { + terminal.write(String(data.payload)) + } + } + } + + socket.onclose = () => { + console.log('WebSocket Client Closed') + } + + socket.onerror = error => { + console.error('WebSocket Error:', error) + setError(error) + } + + terminal.onData(data => { + if (data === '\u001bOP') { + // const bufferToSend = Buffer.from('\u001b[11~', 'utf8') + // socket.send(JSON.stringify({ type: 'input', payload: bufferToSend })) + socket.send(JSON.stringify({ type: 'input', payload: '\u001b[11~' })) + return + } + // const bufferToSend = Buffer.from(data, 'utf8') + // socket.send(JSON.stringify({ type: 'input', payload: bufferToSend })) + socket.send(JSON.stringify({ type: 'input', payload: data })) + }) + + // terminal.onResize(size => { + // socket.send(JSON.stringify({ type: 'resize', payload: { cols: size.cols, rows: size.rows } })) + // }) + + // eslint-disable-next-line consistent-return + return () => { + terminal.dispose() + if (socket.readyState === WebSocket.OPEN) { + socket.close() + } + } + }, [terminal, endpoint, namespace, podName]) + + return ( + <> + + +
+ + + {isLoading && !error && } + {error && } + + ) +} + +export const TestWs: FC = () => { + const cluster = useSelector((state: RootState) => state.cluster.cluster) + const endpoint = `http://localhost:4002/api/clusters/${cluster}/openapi-bff/terminal/terminalPod/terminalPod` + + return ( + + ) +} diff --git a/src/components/organisms/Factory/TestWs/index.ts b/src/components/organisms/Factory/TestWs/index.ts new file mode 100644 index 0000000..f7cb419 --- /dev/null +++ b/src/components/organisms/Factory/TestWs/index.ts @@ -0,0 +1 @@ +export * from './TestWs' diff --git a/src/components/organisms/Factory/TestWs/styled.ts b/src/components/organisms/Factory/TestWs/styled.ts new file mode 100644 index 0000000..ecb9125 --- /dev/null +++ b/src/components/organisms/Factory/TestWs/styled.ts @@ -0,0 +1,28 @@ +import styled from 'styled-components' + +const FullWidthDiv = styled.div` + display: flex; + justify-content: center; + width: 100%; +` + +type TCustomCardProps = { + $isVisible?: boolean +} + +const CustomCard = styled.div` + display: ${({ $isVisible }) => ($isVisible ? 'block' : 'none')}; + max-height: calc(100vh - 158px); + margin: 24px; + /* overflow-y: auto; */ + /* background: black; */ + + * { + scrollbar-width: thin; + } +` + +export const Styled = { + FullWidthDiv, + CustomCard, +}