fix issue

pull/369/head
vapao 2021-08-12 01:28:07 +08:00
parent ca17dd6c98
commit 7394c830f6
6 changed files with 111 additions and 106 deletions

View File

@ -14,9 +14,9 @@ import {
FullscreenExitOutlined FullscreenExitOutlined
} from '@ant-design/icons'; } from '@ant-design/icons';
import { Modal, Collapse, Tooltip } from 'antd'; import { Modal, Collapse, Tooltip } from 'antd';
import { X_TOKEN } from 'libs';
import OutView from './OutView'; 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'; import store from './store';
@ -25,9 +25,11 @@ class ExecConsole extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.socket = null; this.socket = null;
this.elements = {}; this.terms = {};
this.outputs = {};
this.state = { 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 = new WebSocket(`${protocol}//${window.location.host}/api/ws/exec/${store.token}/?x-token=${X_TOKEN}`);
this.socket.onopen = () => { this.socket.onopen = () => {
this.socket.send('ok'); this.socket.send('ok');
for (let item of Object.values(store.outputs)) {
item['system'].push('### Waiting for schedule\n')
}
}; };
this.socket.onmessage = e => { this.socket.onmessage = e => {
if (e.data === 'pong') { if (e.data === 'pong') {
this.socket.send('ping') this.socket.send('ping')
} else { } else {
const {key, data, type, status} = JSON.parse(e.data); const {key, data, status} = JSON.parse(e.data);
if (status !== undefined) { if (status !== undefined) store.outputs[key].status = status;
store.outputs[key]['status'] = status if (data) {
} else if (data) { if (this.terms[key]) {
store.outputs[key][type].push(data); 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; store.isFullscreen = false;
} }
genExtra = (outputs) => { genExtra = (status) => {
let latest, icon; if (status === -2) {
if (outputs['status'] === -2) {
return <LoadingOutlined style={{fontSize: 20, color: '#108ee9'}}/>; return <LoadingOutlined style={{fontSize: 20, color: '#108ee9'}}/>;
} else if (outputs['status'] === 0) { } else if (status === 0) {
latest = outputs['info'][outputs['info'].length - 1]; return <CheckCircleTwoTone style={{fontSize: 20}} twoToneColor="#52c41a"/>
icon = <CheckCircleTwoTone style={{fontSize: 20}} twoToneColor="#52c41a"/>
} else { } else {
latest = outputs['error'][outputs['error'].length - 1] return (
icon = <Tooltip title={`退出状态码:${outputs['status']}`}> <Tooltip title={`退出状态码:${status}`}>
<WarningTwoTone style={{fontSize: 20}} twoToneColor="red"/> <WarningTwoTone style={{fontSize: 20}} twoToneColor="red"/>
</Tooltip> </Tooltip>
}
return (
<div style={{display: 'flex', alignItems: 'center'}}>
<pre className={styles.header}>{latest}</pre>
{icon}
</div>
) )
}
}; };
handleUpdate = (data) => {
this.setState(data, () => {
const key = this.state.activeKey;
if (key && this.terms[key]) setTimeout(this.terms[key].fit)
})
}
render() { render() {
const {isFullscreen, activeKey} = this.state;
return ( return (
<Modal <Modal
visible visible
width={store.isFullscreen ? '100%' : 1000} width={isFullscreen ? '100%' : 1000}
title={[ title={[
<span key="1">执行控制台</span>, <span key="1">执行控制台</span>,
<div key="2" className={styles.fullscreen} onClick={() => store.isFullscreen = !store.isFullscreen}> <div key="2" className={styles.fullscreen} onClick={() => this.handleUpdate({isFullscreen: !isFullscreen})}>
{store.isFullscreen ? <FullscreenExitOutlined/> : <FullscreenOutlined/>} {isFullscreen ? <FullscreenExitOutlined/> : <FullscreenOutlined/>}
</div> </div>
]} ]}
footer={null} footer={null}
@ -97,15 +112,16 @@ class ExecConsole extends React.Component {
maskClosable={false}> maskClosable={false}>
<Collapse <Collapse
accordion accordion
defaultActiveKey={[0]}
className={styles.collapse} className={styles.collapse}
activeKey={activeKey}
onChange={key => this.handleUpdate({activeKey: key})}
expandIcon={({isActive}) => <CaretRightOutlined style={{fontSize: 16}} rotate={isActive ? 90 : 0}/>}> expandIcon={({isActive}) => <CaretRightOutlined style={{fontSize: 16}} rotate={isActive ? 90 : 0}/>}>
{Object.entries(store.outputs).map(([key, item], index) => ( {Object.entries(store.outputs).map(([key, item], index) => (
<Collapse.Panel <Collapse.Panel key={key} header={<b>{item['title']}</b>} extra={this.genExtra(item.status)}>
key={index} <OutView
header={<b>{item['title']}</b>} isFullscreen={isFullscreen}
extra={this.genExtra(item)}> getOutput={() => this.outputs[key]}
<OutView outputs={item}/> setTerm={term => this.terms[key] = term}/>
</Collapse.Panel> </Collapse.Panel>
))} ))}
</Collapse> </Collapse>

View File

@ -3,36 +3,33 @@
* Copyright (c) <spug.dev@gmail.com> * Copyright (c) <spug.dev@gmail.com>
* Released under the AGPL-3.0 License. * Released under the AGPL-3.0 License.
*/ */
import React from 'react'; import React, { useEffect, useRef } from 'react';
import { toJS } from 'mobx'; import { FitAddon } from 'xterm-addon-fit';
import { observer } from 'mobx-react'; import { Terminal } from 'xterm';
import styles from './index.module.css';
import store from './store';
@observer function OutView(props) {
class OutView extends React.Component { const el = useRef()
constructor(props) {
super(props); useEffect(() => {
this.el = null; 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
}, [])
componentDidUpdate(prevProps, prevState, snapshot) {
setTimeout(() => {
if (this.el) this.el.scrollTop = this.el.scrollHeight
}, 100)
}
render() {
const outputs = toJS(this.props.outputs);
const maxHeight = store.isFullscreen ? 500 : 300;
return ( return (
<div ref={ref => this.el = ref} className={styles.console} style={{maxHeight}}> <div ref={el} style={{padding: '10px 15px'}}/>
<pre style={{color: '#91d5ff'}}>{outputs['system']}</pre>
<pre>{outputs['info']}</pre>
<pre style={{color: '#ffa39e'}}>{outputs['error']}</pre>
</div>
) )
}
} }
export default OutView export default OutView

View File

@ -53,7 +53,7 @@ class TaskIndex extends React.Component {
icon={<PlusOutlined/>} icon={<PlusOutlined/>}
onClick={() => store.showHost = true}>从主机列表中选择</Button> onClick={() => store.showHost = true}>从主机列表中选择</Button>
<Form.Item label="执行命令"> <Form.Item label="执行命令">
<ACEditor mode="sh" value={body} height="300px" onChange={body => this.setState({body})}/> <ACEditor mode="sh" value={body} height="300px" width="700px" onChange={body => this.setState({body})}/>
</Form.Item> </Form.Item>
<Form.Item> <Form.Item>
<Button icon={<PlusOutlined/>} onClick={store.switchTemplate}>从执行模版中选择</Button> <Button icon={<PlusOutlined/>} onClick={store.switchTemplate}>从执行模版中选择</Button>

View File

@ -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;
}

View File

@ -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;
}

View File

@ -10,7 +10,6 @@ class Store {
@observable outputs = {}; @observable outputs = {};
@observable host_ids = []; @observable host_ids = [];
@observable token = null; @observable token = null;
@observable isFullscreen = false;
@observable showHost = false; @observable showHost = false;
@observable showConsole = false; @observable showConsole = false;
@observable showTemplate = false; @observable showTemplate = false;
@ -33,9 +32,6 @@ class Store {
const key = `${host.hostname}:${host.port}`; const key = `${host.hostname}:${host.port}`;
this.outputs[key] = { this.outputs[key] = {
title: `${host.name}(${key})`, title: `${host.name}(${key})`,
system: ['### Establishing communication\n'],
info: [],
error: [],
status: -2 status: -2
} }
} }