pod terminal

This commit is contained in:
typescreep
2025-07-21 22:44:21 +03:00
parent 5988906cff
commit 2261f321ee
7 changed files with 36 additions and 207 deletions

33
package-lock.json generated
View File

@@ -11,13 +11,14 @@
"@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.63",
"@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",
"@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",
@@ -39,6 +40,7 @@
"typescript": "4.9.5",
"usehooks-ts": "3.1.1",
"uuid": "11.0.3",
"xterm-theme": "1.1.0",
"yaml": "2.7.0"
},
"devDependencies": {
@@ -2800,13 +2802,16 @@
}
},
"node_modules/@prorobotech/openapi-k8s-toolkit": {
"version": "0.0.1-alpha.62",
"resolved": "https://registry.npmjs.org/@prorobotech/openapi-k8s-toolkit/-/openapi-k8s-toolkit-0.0.1-alpha.62.tgz",
"integrity": "sha512-Ase/PSMID8NKuZhNMn19f4rWluKlaAA62YOxY7CJq0ji1DP4hQuw3Izw8Qm/CFAMJ/J9wdnyEgFraEZ9qAeP+g==",
"version": "0.0.1-alpha.63",
"resolved": "https://registry.npmjs.org/@prorobotech/openapi-k8s-toolkit/-/openapi-k8s-toolkit-0.0.1-alpha.63.tgz",
"integrity": "sha512-eLrSeLkswEE287T3U0sGm1UhoiVJrSvZdx6DsIfOxoKYfnpa8bMArh9/baNOSc2f/vivzn+eksuCfm4No0N5yA==",
"license": "MIT",
"dependencies": {
"@monaco-editor/react": "4.6.0",
"@reduxjs/toolkit": "2.2.5",
"@xterm/addon-attach": "0.11.0",
"@xterm/addon-fit": "0.10.0",
"@xterm/xterm": "5.5.0",
"axios": "1.4.0",
"cross-env": "7.0.3",
"dotenv": "16.4.7",
@@ -2818,6 +2823,7 @@
"typescript": "4.9.5",
"usehooks-ts": "3.1.1",
"uuid": "11.0.3",
"xterm-theme": "1.1.0",
"yaml": "2.7.0"
},
"peerDependencies": {
@@ -4438,6 +4444,15 @@
"vite": "^4 || ^5"
}
},
"node_modules/@xterm/addon-attach": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@xterm/addon-attach/-/addon-attach-0.11.0.tgz",
"integrity": "sha512-JboCN0QAY6ZLY/SSB/Zl2cQ5zW1Eh4X3fH7BnuR1NB7xGRhzbqU2Npmpiw/3zFlxDaU88vtKzok44JKi2L2V2Q==",
"license": "MIT",
"peerDependencies": {
"@xterm/xterm": "^5.0.0"
}
},
"node_modules/@xterm/addon-fit": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz",
@@ -13529,6 +13544,12 @@
"node": ">=0.4"
}
},
"node_modules/xterm-theme": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/xterm-theme/-/xterm-theme-1.1.0.tgz",
"integrity": "sha512-n2GocBEbqcz4vEl4OYkU93hEVia8GWdnqchiz/0nQ/olRUyhulGf4wfha23x/D2m0imWaIavRZtt8c6kWZXdsA==",
"license": "ISC"
},
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",

View File

@@ -20,7 +20,7 @@
"@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.63",
"@readme/openapi-parser": "4.0.0",
"@reduxjs/toolkit": "2.2.5",
"@tanstack/react-query": "5.62.2",
@@ -49,6 +49,7 @@
"typescript": "4.9.5",
"usehooks-ts": "3.1.1",
"uuid": "11.0.3",
"xterm-theme": "1.1.0",
"yaml": "2.7.0"
},
"devDependencies": {

View File

@@ -12,7 +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'
import '@xterm/xterm/css/xterm.css'
type TFactoryProps = {
setSidebarTags: (tags: string[]) => void
@@ -60,7 +60,6 @@ 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

@@ -1,169 +0,0 @@
/* 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

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

View File

@@ -1,28 +0,0 @@
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,
}

View File

@@ -149,6 +149,12 @@ export default defineConfig({
// })
// },
},
'^/api/clusters/.*/openapi-bff-ws': {
target: options?.BFF_URL,
changeOrigin: true,
secure: false,
ws: true,
},
},
},
})