diff --git a/spug_web/src/pages/exec/task/ExecConsole.js b/spug_web/src/pages/exec/task/ExecConsole.js index f26dced..54cfa7f 100644 --- a/spug_web/src/pages/exec/task/ExecConsole.js +++ b/spug_web/src/pages/exec/task/ExecConsole.js @@ -14,9 +14,9 @@ import { FullscreenExitOutlined } from '@ant-design/icons'; import { Modal, Collapse, Tooltip } from 'antd'; -import { X_TOKEN } from 'libs'; import OutView from './OutView'; -import styles from './index.module.css'; +import { X_TOKEN } from 'libs'; +import styles from './index.module.less'; import store from './store'; @@ -25,9 +25,11 @@ class ExecConsole extends React.Component { constructor(props) { super(props); this.socket = null; - this.elements = {}; + this.terms = {}; + this.outputs = {}; this.state = { - data: {} + activeKey: Object.keys(store.outputs)[0], + isFullscreen: false } } @@ -36,19 +38,31 @@ class ExecConsole extends React.Component { this.socket = new WebSocket(`${protocol}//${window.location.host}/api/ws/exec/${store.token}/?x-token=${X_TOKEN}`); this.socket.onopen = () => { this.socket.send('ok'); - for (let item of Object.values(store.outputs)) { - item['system'].push('### Waiting for schedule\n') - } }; this.socket.onmessage = e => { if (e.data === 'pong') { this.socket.send('ping') } else { - const {key, data, type, status} = JSON.parse(e.data); - if (status !== undefined) { - store.outputs[key]['status'] = status - } else if (data) { - store.outputs[key][type].push(data); + const {key, data, status} = JSON.parse(e.data); + if (status !== undefined) store.outputs[key].status = status; + if (data) { + if (this.terms[key]) { + this.terms[key].write(data) + } else if (this.outputs[key]) { + this.outputs[key] += data + } else { + this.outputs[key] = data + } + } + } + }; + this.socket.onerror = () => { + for (let key of Object.keys(store.outputs)) { + store.outputs[key]['status'] = 'websocket error' + if (this.terms[key]) { + this.terms[key].write('Websocket connection failed!') + } else { + this.outputs[key] = 'Websocket connection failed!' } } } @@ -59,36 +73,37 @@ class ExecConsole extends React.Component { store.isFullscreen = false; } - genExtra = (outputs) => { - let latest, icon; - if (outputs['status'] === -2) { + genExtra = (status) => { + if (status === -2) { return ; - } else if (outputs['status'] === 0) { - latest = outputs['info'][outputs['info'].length - 1]; - icon = + } else if (status === 0) { + return } else { - latest = outputs['error'][outputs['error'].length - 1] - icon = - - + return ( + + + + ) } - return ( -
-
{latest}
- {icon} -
- ) }; + handleUpdate = (data) => { + this.setState(data, () => { + const key = this.state.activeKey; + if (key && this.terms[key]) setTimeout(this.terms[key].fit) + }) + } + render() { + const {isFullscreen, activeKey} = this.state; return ( 执行控制台, -
store.isFullscreen = !store.isFullscreen}> - {store.isFullscreen ? : } +
this.handleUpdate({isFullscreen: !isFullscreen})}> + {isFullscreen ? : }
]} footer={null} @@ -97,15 +112,16 @@ class ExecConsole extends React.Component { maskClosable={false}> this.handleUpdate({activeKey: key})} expandIcon={({isActive}) => }> {Object.entries(store.outputs).map(([key, item], index) => ( - {item['title']}} - extra={this.genExtra(item)}> - + {item['title']}} extra={this.genExtra(item.status)}> + this.outputs[key]} + setTerm={term => this.terms[key] = term}/> ))} diff --git a/spug_web/src/pages/exec/task/OutView.js b/spug_web/src/pages/exec/task/OutView.js index 35894e9..061eb87 100644 --- a/spug_web/src/pages/exec/task/OutView.js +++ b/spug_web/src/pages/exec/task/OutView.js @@ -3,36 +3,33 @@ * Copyright (c) * Released under the AGPL-3.0 License. */ -import React from 'react'; -import { toJS } from 'mobx'; -import { observer } from 'mobx-react'; -import styles from './index.module.css'; -import store from './store'; +import React, { useEffect, useRef } from 'react'; +import { FitAddon } from 'xterm-addon-fit'; +import { Terminal } from 'xterm'; -@observer -class OutView extends React.Component { - constructor(props) { - super(props); - this.el = null; - } +function OutView(props) { + const el = useRef() - componentDidUpdate(prevProps, prevState, snapshot) { - setTimeout(() => { - if (this.el) this.el.scrollTop = this.el.scrollHeight - }, 100) - } + useEffect(() => { + const fitPlugin = new FitAddon() + const term = new Terminal({disableStdin: true}) + term.loadAddon(fitPlugin) + term.setOption('theme', {background: '#fff', foreground: '#000', selection: '#999'}) + term.open(el.current) + const data = props.getOutput() + if (data) term.write(data) + term.fit = () => { + const dimensions = fitPlugin.proposeDimensions() + if (dimensions.cols && dimensions.rows) fitPlugin.fit() + } + props.setTerm(term) + fitPlugin.fit() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) - render() { - const outputs = toJS(this.props.outputs); - const maxHeight = store.isFullscreen ? 500 : 300; - return ( -
this.el = ref} className={styles.console} style={{maxHeight}}> -
{outputs['system']}
-
{outputs['info']}
-
{outputs['error']}
-
- ) - } + return ( +
+ ) } export default OutView \ No newline at end of file diff --git a/spug_web/src/pages/exec/task/index.js b/spug_web/src/pages/exec/task/index.js index f821f12..c6e8c67 100644 --- a/spug_web/src/pages/exec/task/index.js +++ b/spug_web/src/pages/exec/task/index.js @@ -53,7 +53,7 @@ class TaskIndex extends React.Component { icon={} onClick={() => store.showHost = true}>从主机列表中选择 - this.setState({body})}/> + this.setState({body})}/> diff --git a/spug_web/src/pages/exec/task/index.module.css b/spug_web/src/pages/exec/task/index.module.css deleted file mode 100644 index 1230594..0000000 --- a/spug_web/src/pages/exec/task/index.module.css +++ /dev/null @@ -1,37 +0,0 @@ -.collapse :global(.ant-collapse-content-box) { - padding: 0; -} - -.fullscreen { - position: absolute; - top: 0; - right: 0; - display: block; - width: 56px; - height: 56px; - line-height: 56px; - text-align: center; - cursor: pointer; - color: rgba(0, 0, 0, .45); - margin-right: 56px; -} -.fullscreen:hover { - color: #000; -} - -.console { - padding: 10px 15px; - overflow: scroll; -} - -.header { - overflow: hidden; - text-overflow: ellipsis; - max-width: 400px; - padding-right: 20px; - margin: 0 -} - -pre { - margin: 0; -} \ No newline at end of file diff --git a/spug_web/src/pages/exec/task/index.module.less b/spug_web/src/pages/exec/task/index.module.less new file mode 100644 index 0000000..5fb30ab --- /dev/null +++ b/spug_web/src/pages/exec/task/index.module.less @@ -0,0 +1,33 @@ +.collapse :global(.ant-collapse-content-box) { + padding: 0; +} + +.fullscreen { + position: absolute; + top: 0; + right: 0; + display: block; + width: 56px; + height: 56px; + line-height: 56px; + text-align: center; + cursor: pointer; + color: rgba(0, 0, 0, .45); + margin-right: 56px; +} + +.fullscreen:hover { + color: #000; +} + +.header { + overflow: hidden; + text-overflow: ellipsis; + max-width: 400px; + padding-right: 20px; + margin: 0 +} + +pre { + margin: 0; +} \ No newline at end of file diff --git a/spug_web/src/pages/exec/task/store.js b/spug_web/src/pages/exec/task/store.js index b8b0d8d..b05d4d8 100644 --- a/spug_web/src/pages/exec/task/store.js +++ b/spug_web/src/pages/exec/task/store.js @@ -10,7 +10,6 @@ class Store { @observable outputs = {}; @observable host_ids = []; @observable token = null; - @observable isFullscreen = false; @observable showHost = false; @observable showConsole = false; @observable showTemplate = false; @@ -33,9 +32,6 @@ class Store { const key = `${host.hostname}:${host.port}`; this.outputs[key] = { title: `${host.name}(${key})`, - system: ['### Establishing communication\n'], - info: [], - error: [], status: -2 } }