diff --git a/package-lock.json b/package-lock.json index f5aabc0e6..2bb26e24a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "educoder", + "name": "forge", "version": "0.1.0", "lockfileVersion": 1, "requires": true, @@ -3888,11 +3888,6 @@ "randomfill": "^1.0.3" } }, - "crypto-js": { - "version": "4.0.0", - "resolved": "https://registry.npm.taobao.org/crypto-js/download/crypto-js-4.0.0.tgz", - "integrity": "sha1-KQSrJnep0EKFai6i74DekuSjbcw=" - }, "crypto-random-string": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", @@ -20480,6 +20475,16 @@ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", "dev": true }, + "xterm": { + "version": "4.8.1", + "resolved": "https://registry.npm.taobao.org/xterm/download/xterm-4.8.1.tgz", + "integrity": "sha1-FVoXKaQ+Gom0BlJOIsVjQznjnKE=" + }, + "xterm-addon-fit": { + "version": "0.4.0", + "resolved": "https://registry.npm.taobao.org/xterm-addon-fit/download/xterm-addon-fit-0.4.0.tgz", + "integrity": "sha1-BuDF0KaqrPsAnvVl76HIHpPZAZM=" + }, "y18n": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", diff --git a/package.json b/package.json index edc6e18e7..5ad750143 100644 --- a/package.json +++ b/package.json @@ -111,7 +111,9 @@ "webpack-dev-server": "^3.10.3", "webpack-manifest-plugin": "^2.2.0", "whatwg-fetch": "2.0.3", - "wrap-md-editor": "^0.2.20" + "wrap-md-editor": "^0.2.20", + "xterm": "4.8.1", + "xterm-addon-fit": "0.4.0" }, "scripts": { "start": "node --max_old_space_size=15360 scripts/start.js", diff --git a/src/forge/DevOps/OpsDetailRightpanel.jsx b/src/forge/DevOps/OpsDetailRightpanel.jsx index 6998964e2..3546312d9 100644 --- a/src/forge/DevOps/OpsDetailRightpanel.jsx +++ b/src/forge/DevOps/OpsDetailRightpanel.jsx @@ -2,6 +2,7 @@ import React, { useState, useEffect } from "react"; import { Spin , Menu } from "antd"; import { FlexAJ, AlignCenter } from "../Component/layout"; import axios from "axios"; +import CodeSSH from './ssh/Index'; export default ({ data, @@ -68,10 +69,10 @@ export default ({ } return ( - {/* {setNav(e.key)}} selectedKeys={[nav]} mode="horizontal"> + {setNav(e.key)}} selectedKeys={[nav]} mode="horizontal"> 文件 命令行 - */} + { nav === "0" &&
@@ -111,7 +112,9 @@ export default ({ )}
} - + { + nav === "1" && + }
); }; diff --git a/src/forge/DevOps/ssh/Index.jsx b/src/forge/DevOps/ssh/Index.jsx new file mode 100644 index 000000000..64fb5b4ef --- /dev/null +++ b/src/forge/DevOps/ssh/Index.jsx @@ -0,0 +1,14 @@ +import React, { useState } from 'react'; +import XmlPanel from './XmlPanel'; +import mediator from './mediator'; + +function Index(){ + const [ sshConfigData ,setSshConfigData ] = useState(undefined); + + return( +
+ +
+ ) +} +export default Index; \ No newline at end of file diff --git a/src/forge/DevOps/ssh/XmlPanel.jsx b/src/forge/DevOps/ssh/XmlPanel.jsx new file mode 100644 index 000000000..5e2a5f50a --- /dev/null +++ b/src/forge/DevOps/ssh/XmlPanel.jsx @@ -0,0 +1,218 @@ +import React, { useRef, useEffect, useState } from 'react'; +import { Base64 } from 'js-base64'; + +import { Terminal } from 'xterm'; +import 'xterm/css/xterm.css'; +import mediator from './mediator'; +import ResizeObserver from 'resize-observer-polyfill'; + +function getColsAndRows(width, height, term) { + let w = term._core._renderService.dimensions.actualCellWidth || 9.5; + let h = term._core._renderService.dimensions.actualCellHeight || 18; + const rows = Math.floor(height / h); + const cols = Math.floor(width / w); + return [cols, rows]; +} + +function onLayout(term, el) { + const ro = new ResizeObserver(entries => { + for (let entry of entries) { + if (entry.target.offsetHeight > 0 || entry.target.offsetWidth > 0) { + const [cols, rows] = getColsAndRows( + entry.target.offsetWidth, + entry.target.offsetHeight, + term, + ); + console.log('cols, rows', cols, rows); + term.resize(cols, rows); + mediator.publish('ssh-xterm-resize', { + columns: cols, + rows: rows, + width: entry.target.offsetWidth, + height: entry.target.offsetHeight, + }); + } + } + }); + ro.observe(el); + return ro; +} + +const TimeTicket = 30000; + +//建立 websockt 来交互 +//根据容器大小计算行数和列数并做到自适应 +//socket 与 term 需要分开初始化 因为socket 可能重置连接 +//mediator 监听消息,如果和id匹配,则建立连接,重置,或关闭连接 + +export default ({ sshConfigData, sid }) => { + const [term, setTerm] = useState(null); + + const { ws_url, password, port } = sshConfigData; + const el = useRef(); + const socket = useRef(); + const isFirstConnected = useRef(false); + + //term init + useEffect(() => { + if (el.current && ws_url) { + const term = new Terminal({ fontSize: 16, rendererType: 'dom' }); + term.open(el.current); + + term.onData(data => { + if (socket.current) { + if (socket.current.readyState === 1) { + socket.current.send(JSON.stringify({ tp: 'client', data: data })); + mediator.publish('on-operating-ssh'); //有操作则自动延时 + } else { + //断开连接后重连 + // socket.current = null + // mediator.publish('create-socket', sid) + } + } + }); + term.write('Connecting...'); + setTerm(term); + const ro = onLayout(term, el.current); + return () => { + term.dispose(); + ro.unobserve(el.current); + }; + } + }, [ws_url, el.current]); + + useEffect(() => { + if (term && ws_url) { + function createSocket() { + const socketInstance = new WebSocket(ws_url); + socket.current = socketInstance; + + socketInstance.onopen = () => { + let container = term.element.parentElement; + if (container) { + let width = container.offsetWidth; + let height = container.offsetHeight; + console.log('init', { + tp: 'init', + data: { + ...sshConfigData, + secret: password, + width, + height, + rows: term.rows, + columns: term.cols, + }, + }); + socketInstance.send( + JSON.stringify({ + tp: 'init', + data: { + ...sshConfigData, + secret: password, + width, + height, + rows: term.rows, + columns: term.cols, + }, + }), + ); + } + term.focus(); + }; + socketInstance.onerror = error => { + console.log( + '------in socket error----', + error, + socketInstance, + ws_url, + ); + //连接报错后,重新请求资源 + // mediator.publish('on-recreate-socket') + }; + socketInstance.onmessage = event => { + if (!isFirstConnected.current) { + term.write('\r'); + // term.focus() + setTimeout(() => { + // term.clear(); + }, 1000); + } + isFirstConnected.current = true; + console.log('event:', event); + + const data = Base64.decode(event.data.toString()); + let w = term._core._renderService.dimensions.actualCellWidth || 9.5; + + console.log('data:', data, w, term); + term.write(data); + }; + + socketInstance.onclose = evt => { + if (tid) { + clearInterval(tid); + } + term.write('\r\nconnection closed'); + }; + } + + const tid = setInterval(() => { + if (socket.current) { + socket.current.send(JSON.stringify({ tp: 'h' })); + } + }, TimeTicket); + + const unSubCreate = mediator.subscribe('create-socket', id => { + if (sid === id) { + if (socket.current && socket.current.readyState === 1) { + term.focus(); + } else { + createSocket(); + } + term.focus(); + } + }); + + const unSubClose = mediator.subscribe('close-socket', id => { + if (sid === id) { + if (socket.current) { + socket.current.close(); + isFirstConnected.current = false; + term.clear(); + } + socket.current = null; + } + }); + + const unSubResize = mediator.subscribe('ssh-xterm-resize', option => { + if (socket.current && socket.current.readyState === 1) { + socket.current.send( + JSON.stringify({ tp: 'resize', data: { ...option } }), + ); + } + }); + + const unSubAddTime = mediator.subscribe('ssh-add-connect-time', () => { + if (socket.current && socket.current.readyState === 1) { + socket.current.send(JSON.stringify({ tp: 'overtime' })); + } + }); + + return () => { + unSubClose(); + unSubCreate(); + unSubResize(); + unSubAddTime(); + if (socket.current) { + socket.current.close(); + isFirstConnected.current = false; + } + }; + } + }, [term, ws_url, port]); + + return ( +
+ {!ws_url ?

正在连接命令行服务...

: null} +
+ ); +}; diff --git a/src/forge/DevOps/ssh/mediator.jsx b/src/forge/DevOps/ssh/mediator.jsx new file mode 100644 index 000000000..652705799 --- /dev/null +++ b/src/forge/DevOps/ssh/mediator.jsx @@ -0,0 +1,46 @@ +function Mediator(obj) { + const channels = {}; + + const mediator = { + subscribe: function(channel, cb) { + if (!channels[channel]) { + channels[channel] = []; + } + channels[channel].push(cb); + return this.unsubscribe.bind(null, channel, cb); + }, + + unsubscribe: function(channel, cb) { + let rs = channels[channel]; + let index = -1; + if (rs) { + for (let i = 0; i < rs.length; i++) { + if (rs[i].name === cb.name) { + index = i; + break; + } + } + if (index >= 0) { + channels[channel].splice(index, 1); + return true; + } + } + return false; + }, + + publish: function(channel) { + if (!channels[channel]) { + return false; + } + const args = Array.prototype.slice.call(arguments, 1); + channels[channel].forEach(subscription => { + subscription.apply(null, args); + }); + return this; + }, + }; + + return obj ? Object.assign(obj, mediator) : mediator; +} +const mediator = new Mediator(); +export default mediator;