Skip to content

Commit

Permalink
feat(xterm): integrate xterm into connection for cluster or tenant
Browse files Browse the repository at this point in the history
  • Loading branch information
powerfooI committed Apr 3, 2024
1 parent 2021b23 commit 3d513c4
Show file tree
Hide file tree
Showing 8 changed files with 285 additions and 5 deletions.
9 changes: 8 additions & 1 deletion ui/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,12 @@ export default defineConfig({
},
routes,
history: { type: 'hash' },
npmClient: 'yarn'
npmClient: 'yarn',
proxy: {
'/api': {
target: 'http://11.161.204.4:18083',
changeOrigin: true,
ws: true,
}
}
});
1 change: 1 addition & 0 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"@antv/g6-react-node": "^1.4.5",
"@oceanbase/ui": "^0.2.13",
"@umijs/max": "^4.0.72",
"@xterm/xterm": "^5.4.0",
"ahooks": "^3.7.8",
"antd": "^5.4.0",
"copy-to-clipboard": "^3.3.3",
Expand Down
96 changes: 96 additions & 0 deletions ui/src/components/Terminal/terminal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { Terminal } from '@xterm/xterm'
import { Button, Modal } from 'antd'
import React from 'react'

export interface ITerminal {
terminalId: string
onClose?: () => void
onConnected?: () => void
}

function devLog(...args: any[]) {
if (process.env.NODE_ENV === 'development') {
console.log(...args)
}
}

export const OBTerminal: React.FC<ITerminal> = (props) => {
const { terminalId } = props
const ref = React.useRef<HTMLDivElement>(null)
const [ws, setWs] = React.useState<WebSocket | null>(null)

React.useEffect(() => {
if (ref.current) {
const term = new Terminal({
cols: 160,
rows: 60,
})
term.open(ref.current)

if (term.options.fontSize) {
const containerWidth = ref.current.clientWidth

const cols = Math.floor(containerWidth / 9.2)
const rows = Math.floor(cols / 4)
term.resize(cols, rows)
const ws = new WebSocket(`ws://${location.host}/api/v1/terminal/${terminalId}?cols=${cols}&rows=${rows}`)
term.write('Hello from \x1B[1;3;31mOceanBase\x1B[0m\r\n')

ws.onopen = function () {
devLog('Websocket connection open ...')
// ws.send(JSON.stringify({ type: 'ping' }))
term.onData(function (data) {
ws.send(data)
})
props.onConnected?.()
setWs(ws)

window.addEventListener('beforeunload', () => {
if (ws) {
ws.close()
props.onClose?.()
setWs(null)
}
})
}

ws.onmessage = function (event) {
term.write(event.data)
}

ws.onclose = function () {
devLog('Connection closed.')
term.write('\r\nConnection closed.\r\n')
}

ws.onerror = function (evt) {
console.error('WebSocket error observed:', evt)
}
}
}

return () => {
window.removeEventListener('beforeunload', () => { })
}
}, [])

return (
<>
{ws && <div style={{ marginBottom: 12 }}>
<Button danger type="primary" onClick={() => {
Modal.confirm({
title: '断开连接',
content: '确定要断开连接吗?',
okType: 'danger',
onOk: () => {
ws.close()
props.onClose?.()
setWs(null)
}
})
}}>断开连接</Button>
</div>}
<div ref={ref} />
</>
)
}
74 changes: 74 additions & 0 deletions ui/src/pages/Cluster/Detail/Connection/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import React, { useState } from 'react'
import { PageContainer } from '@ant-design/pro-components'
import { intl } from '@/utils/intl'
import { OBTerminal } from '@/components/Terminal/terminal'
import { Button, Row, message } from 'antd'
import { request } from '@umijs/max'
import { useRequest } from 'ahooks'
import { getNSName } from '../Overview/helper'
import { getClusterDetailReq } from '@/services'
import BasicInfo from '../Overview/BasicInfo'


const ClusterConnection: React.FC = () => {
const header = () => {
return {
title: intl.formatMessage({
id: 'dashboard.Cluster.Detail.Connection',
defaultMessage: '集群连接',
})
}
}
const [[ns, name]] = useState(getNSName())

const { data: clusterDetail } = useRequest(getClusterDetailReq, {
defaultParams: [{ name, ns }],
})

const { runAsync } = useRequest(async (): Promise<{
data: { terminalId: string }
}> => {
return request(`/api/v1/obclusters/namespace/${ns}/name/${name}/terminal`, {
method: 'PUT'
})
}, {
manual: true
})

const [terminalId, setTerminalId] = useState<string>()

return (
<PageContainer header={header()}>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/xterm/css/xterm.css"
/>
<Row gutter={[16, 16]}>
{clusterDetail && (
<BasicInfo {...(clusterDetail.info as API.ClusterInfo)} />
)}
<div style={{margin: 12, width: '100%'}}>
{terminalId ? (
<OBTerminal terminalId={terminalId} onClose={() => {
setTerminalId(undefined)
message.info('连接已关闭')
}} />
) : (
<Button onClick={async () => {
if ((clusterDetail.info as API.ClusterInfo).status === 'failed') {
message.error('集群未运行')
return
}
const res = await runAsync()
if (res?.data?.terminalId) {
setTerminalId(res.data.terminalId)
}
}}>创建连接</Button>
)}
</div>
</Row>
</PageContainer>
)
}

export default ClusterConnection
8 changes: 8 additions & 0 deletions ui/src/pages/Cluster/Detail/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,14 @@ const ClusterDetail: React.FC = () => {
key: 'tenant',
link: `/cluster/${ns}/${name}/${clusterName}/tenant`,
},
{
title: intl.formatMessage({
id: 'Dashboard.Cluster.Detail.Connection',
defaultMessage: '连接集群',
}),
key: 'connection',
link: `/cluster/${clusterId}/connection`,
},
];

const userMenu = (
Expand Down
81 changes: 81 additions & 0 deletions ui/src/pages/Tenant/Detail/Connection/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import React, { useEffect, useState } from 'react'
import { PageContainer } from '@ant-design/pro-components'
import { intl } from '@/utils/intl'
import { OBTerminal } from '@/components/Terminal/terminal'
import { Button, Row, message } from 'antd'
import { request } from '@umijs/max'
import { useRequest } from 'ahooks'
import { getNSName } from '@/pages/Cluster/Detail/Overview/helper';
import BasicInfo from '../Overview/BasicInfo'
import { getTenant } from '@/services/tenant'


const TenantConnection: React.FC = () => {
const header = () => {
return {
title: intl.formatMessage({
id: 'dashboard.Tenant.Detail.Connection',
defaultMessage: '连接租户',
})
}
}

const [[ns, name]] = useState(getNSName());

const { data: tenantDetailResponse, run: getTenantDetail } = useRequest(getTenant, {
manual: true,
});

const { runAsync } = useRequest(async (): Promise<{
data: { terminalId: string }
}> => {
return request(`/api/v1/obtenants/${ns}/${name}/terminal`, {
method: 'PUT'
})
}, {
manual: true
})

useEffect(() => {
getTenantDetail({ ns, name });
}, []);

const [terminalId, setTerminalId] = useState<string>()

const tenantDetail = tenantDetailResponse?.data;

return (
<PageContainer header={header()}>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/xterm/css/xterm.css"
/>
<Row gutter={[16, 16]}>
{tenantDetail && (
<BasicInfo info={tenantDetail.info} source={tenantDetail.source} />
)}
<div style={{margin: 12, width: '100%'}}>
{terminalId ? (
<OBTerminal terminalId={terminalId} onClose={() => {
setTerminalId(undefined)
message.info('连接已关闭')
}} />
) : (
<Button onClick={async () => {
if (!tenantDetail || tenantDetail.info.status === 'failed') {
message.error('租户未正常运行')
return
}
const res = await runAsync()
if (res?.data?.terminalId) {
setTerminalId(res.data.terminalId)
}
}}>创建连接</Button>
)}
</div>
</Row>
</PageContainer>
)
}

export default TenantConnection
8 changes: 8 additions & 0 deletions ui/src/pages/Tenant/Detail/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,14 @@ const TenantDetail: React.FC = () => {
key: 'monitor',
link: `/tenant/${ns}/${name}/${tenantName}/monitor`,
},
{
title: intl.formatMessage({
id: 'Dashboard.Tenant.Detail.Connection',
defaultMessage: '连接租户',
}),
key: 'connection',
link: `/tenant/${tenantId}/connection`,
},
];

const userMenu = (
Expand Down
13 changes: 9 additions & 4 deletions ui/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3435,6 +3435,11 @@
"@babel/plugin-transform-react-jsx-source" "^7.19.6"
react-refresh "^0.14.0"

"@xterm/xterm@^5.4.0":
version "5.4.0"
resolved "https://registry.npmmirror.com/@xterm/xterm/-/xterm-5.4.0.tgz#a35b585750ca492cbf2a4c99472c480d8c122840"
integrity sha512-GlyzcZZ7LJjhFevthHtikhiDIl8lnTSgol6eTM4aoSNLcuXu3OEhnbqdCVIjtIil3jjabf3gDtb1S8FGahsuEw==

acorn-jsx@^5.3.2:
version "5.3.2"
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
Expand Down Expand Up @@ -6647,10 +6652,10 @@ human-signals@^4.3.0:
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-4.3.1.tgz#ab7f811e851fca97ffbd2c1fe9a958964de321b2"
integrity sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==

i18next@^23.10.0:
version "23.10.0"
resolved "https://registry.npmmirror.com/i18next/-/i18next-23.10.0.tgz#fb328794ae692e6fdde0564259e421f4203c4a2c"
integrity sha512-/TgHOqsa7/9abUKJjdPeydoyDc0oTi/7u9F8lMSj6ufg4cbC1Oj3f/Jja7zj7WRIhEQKB7Q4eN6y68I9RDxxGQ==
i18next@^23.10.1:
version "23.10.1"
resolved "https://registry.npmmirror.com/i18next/-/i18next-23.10.1.tgz#217ce93b75edbe559ac42be00a20566b53937df6"
integrity sha512-NDiIzFbcs3O9PXpfhkjyf7WdqFn5Vq6mhzhtkXzj51aOcNuPNcTwuYNuXCpHsanZGHlHKL35G7huoFeVic1hng==
dependencies:
"@babel/runtime" "^7.23.2"

Expand Down

0 comments on commit 3d513c4

Please sign in to comment.