mirror of https://github.com/openspug/spug
fix issue
parent
ca17dd6c98
commit
7394c830f6
|
@ -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>
|
||||||
|
|
|
@ -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);
|
|
||||||
this.el = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps, prevState, snapshot) {
|
useEffect(() => {
|
||||||
setTimeout(() => {
|
const fitPlugin = new FitAddon()
|
||||||
if (this.el) this.el.scrollTop = this.el.scrollHeight
|
const term = new Terminal({disableStdin: true})
|
||||||
}, 100)
|
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() {
|
return (
|
||||||
const outputs = toJS(this.props.outputs);
|
<div ref={el} style={{padding: '10px 15px'}}/>
|
||||||
const maxHeight = store.isFullscreen ? 500 : 300;
|
)
|
||||||
return (
|
|
||||||
<div ref={ref => this.el = ref} className={styles.console} style={{maxHeight}}>
|
|
||||||
<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
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue