pod terminal wip

This commit is contained in:
typescreep
2025-07-21 20:26:49 +03:00
parent c3a612fc09
commit 5988906cff
6 changed files with 221 additions and 1 deletions

19
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -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<TFactoryProps> = ({ setSidebarTags }) => {
if (spec.withScrollableMainContentCard) {
return (
<ContentCard flexGrow={1} displayFlex flexFlow="column" maxHeight={height}>
<TestWs />
<DynamicRendererWithProviders
urlsToFetch={spec.urlsToFetch}
theme={theme}

View File

@@ -0,0 +1,169 @@
/* eslint-disable no-console */
import React, { FC, useEffect, useState, useRef } from 'react'
import { Result, Spin } from 'antd'
import { Terminal as XTerm } from '@xterm/xterm'
import { FitAddon } from '@xterm/addon-fit'
import '@xterm/xterm/css/xterm.css'
import { useSelector } from 'react-redux'
import type { RootState } from 'store/store'
import { Styled } from './styled'
type TXTerminalProps = {
endpoint: string
namespace: string
podName: string
}
export const XTerminal: FC<TXTerminalProps> = ({ endpoint, namespace, podName }) => {
const [isLoading, setIsLoading] = useState<boolean>(true)
const [error, setError] = useState<Event>()
const [terminal, setTerminal] = useState<XTerm>()
const terminalRef = useRef<HTMLDivElement>(null)
const resizeObserverRef = useRef<ResizeObserver | null>(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 (
<>
<Styled.CustomCard $isVisible={!isLoading && !error}>
<Styled.FullWidthDiv>
<div ref={terminalRef} />
</Styled.FullWidthDiv>
</Styled.CustomCard>
{isLoading && !error && <Spin />}
{error && <Result status="error" title="Error" subTitle={JSON.stringify(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 (
<XTerminal
endpoint={endpoint}
namespace="incloud-web"
podName="incloud-web-web-6cfc645577-thpmc"
key={`${endpoint}`}
/>
)
}

View File

@@ -0,0 +1 @@
export * from './TestWs'

View File

@@ -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<TCustomCardProps>`
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,
}