fix issue

pull/369/head
vapao 2021-08-12 23:46:54 +08:00
parent 8a5c61b841
commit 8365365854
13 changed files with 212 additions and 173 deletions

View File

@ -158,44 +158,44 @@ def _deploy_ext1_host(req, helper, h_id, env):
if not host:
helper.send_error(h_id, 'no such host')
env.update({'SPUG_HOST_ID': h_id, 'SPUG_HOST_NAME': host.hostname})
ssh = host.get_ssh()
code, _ = ssh.exec_command(
f'mkdir -p {extend.dst_repo} && [ -e {extend.dst_dir} ] && [ ! -L {extend.dst_dir} ]')
if code == 0:
helper.send_error(host.id, f'检测到该主机的发布目录 {extend.dst_dir!r} 已存在为了数据安全请自行备份后删除该目录Spug 将会创建并接管该目录。')
# clean
clean_command = f'ls -d {extend.deploy_id}_* 2> /dev/null | sort -t _ -rnk2 | tail -n +{extend.versions + 1} | xargs rm -rf'
helper.remote(host.id, ssh, f'cd {extend.dst_repo} && rm -rf {req.spug_version} && {clean_command}')
# transfer files
tar_gz_file = f'{req.spug_version}.tar.gz'
try:
ssh.put_file(os.path.join(REPOS_DIR, 'build', tar_gz_file), os.path.join(extend.dst_repo, tar_gz_file))
except Exception as e:
helper.send_error(host.id, f'exception: {e}')
with host.get_ssh(default_env=env) as ssh:
code, _ = ssh.exec_command_raw(
f'mkdir -p {extend.dst_repo} && [ -e {extend.dst_dir} ] && [ ! -L {extend.dst_dir} ]')
if code == 0:
helper.send_error(host.id, f'检测到该主机的发布目录 {extend.dst_dir!r} 已存在为了数据安全请自行备份后删除该目录Spug 将会创建并接管该目录。')
# clean
clean_command = f'ls -d {extend.deploy_id}_* 2> /dev/null | sort -t _ -rnk2 | tail -n +{extend.versions + 1} | xargs rm -rf'
helper.remote_raw(host.id, ssh, f'cd {extend.dst_repo} && rm -rf {req.spug_version} && {clean_command}')
# transfer files
tar_gz_file = f'{req.spug_version}.tar.gz'
try:
ssh.put_file(os.path.join(REPOS_DIR, 'build', tar_gz_file), os.path.join(extend.dst_repo, tar_gz_file))
except Exception as e:
helper.send_error(host.id, f'exception: {e}')
command = f'cd {extend.dst_repo} && tar xf {tar_gz_file} && rm -f {req.deploy_id}_*.tar.gz'
helper.remote(host.id, ssh, command)
helper.send_step(h_id, 1, '完成\r\n')
command = f'cd {extend.dst_repo} && tar xf {tar_gz_file} && rm -f {req.deploy_id}_*.tar.gz'
helper.remote_raw(host.id, ssh, command)
helper.send_step(h_id, 1, '完成\r\n')
# pre host
repo_dir = os.path.join(extend.dst_repo, req.spug_version)
if extend.hook_pre_host:
helper.send_step(h_id, 2, f'{human_time()} 发布前任务... \r\n')
command = f'cd {repo_dir} ; {extend.hook_pre_host}'
helper.remote(host.id, ssh, command, env)
# pre host
repo_dir = os.path.join(extend.dst_repo, req.spug_version)
if extend.hook_pre_host:
helper.send_step(h_id, 2, f'{human_time()} 发布前任务... \r\n')
command = f'cd {repo_dir} ; {extend.hook_pre_host}'
helper.remote(host.id, ssh, command)
# do deploy
helper.send_step(h_id, 3, f'{human_time()} 执行发布... ')
helper.remote(host.id, ssh, f'rm -f {extend.dst_dir} && ln -sfn {repo_dir} {extend.dst_dir}')
helper.send_step(h_id, 3, '完成\r\n')
# do deploy
helper.send_step(h_id, 3, f'{human_time()} 执行发布... ')
helper.remote_raw(host.id, ssh, f'rm -f {extend.dst_dir} && ln -sfn {repo_dir} {extend.dst_dir}')
helper.send_step(h_id, 3, '完成\r\n')
# post host
if extend.hook_post_host:
helper.send_step(h_id, 4, f'{human_time()} 发布后任务... \r\n')
command = f'cd {extend.dst_dir} ; {extend.hook_post_host}'
helper.remote(host.id, ssh, command, env)
# post host
if extend.hook_post_host:
helper.send_step(h_id, 4, f'{human_time()} 发布后任务... \r\n')
command = f'cd {extend.dst_dir} ; {extend.hook_post_host}'
helper.remote(host.id, ssh, command)
helper.send_step(h_id, 100, f'\r\n{human_time()} ** 发布成功 **')
helper.send_step(h_id, 100, f'\r\n{human_time()} ** 发布成功 **')
def _deploy_ext2_host(helper, h_id, actions, env, spug_version):
@ -204,31 +204,31 @@ def _deploy_ext2_host(helper, h_id, actions, env, spug_version):
if not host:
helper.send_error(h_id, 'no such host')
env.update({'SPUG_HOST_ID': h_id, 'SPUG_HOST_NAME': host.hostname})
ssh = host.get_ssh()
for index, action in enumerate(actions):
helper.send_step(h_id, 1 + index, f'{human_time()} {action["title"]}...\r\n')
if action.get('type') == 'transfer':
if action.get('src_mode') == '1':
try:
ssh.put_file(os.path.join(REPOS_DIR, env.SPUG_DEPLOY_ID, spug_version), action['dst'])
except Exception as e:
helper.send_error(host.id, f'exception: {e}')
helper.send_info(host.id, 'transfer completed\r\n')
continue
else:
sp_dir, sd_dst = os.path.split(action['src'])
tar_gz_file = f'{spug_version}.tar.gz'
try:
ssh.put_file(os.path.join(sp_dir, tar_gz_file), f'/tmp/{tar_gz_file}')
except Exception as e:
helper.send_error(host.id, f'exception: {e}')
with host.get_ssh(default_env=env) as ssh:
for index, action in enumerate(actions):
helper.send_step(h_id, 1 + index, f'{human_time()} {action["title"]}...\r\n')
if action.get('type') == 'transfer':
if action.get('src_mode') == '1':
try:
ssh.put_file(os.path.join(REPOS_DIR, env.SPUG_DEPLOY_ID, spug_version), action['dst'])
except Exception as e:
helper.send_error(host.id, f'exception: {e}')
helper.send_info(host.id, 'transfer completed\r\n')
continue
else:
sp_dir, sd_dst = os.path.split(action['src'])
tar_gz_file = f'{spug_version}.tar.gz'
try:
ssh.put_file(os.path.join(sp_dir, tar_gz_file), f'/tmp/{tar_gz_file}')
except Exception as e:
helper.send_error(host.id, f'exception: {e}')
command = f'mkdir -p /tmp/{spug_version} && tar xf /tmp/{tar_gz_file} -C /tmp/{spug_version}/ '
command += f'&& rm -rf {action["dst"]} && mv /tmp/{spug_version}/{sd_dst} {action["dst"]} '
command += f'&& rm -rf /tmp/{spug_version}* && echo "transfer completed"'
else:
command = f'cd /tmp ; {action["data"]}'
helper.remote(host.id, ssh, command, env)
command = f'mkdir -p /tmp/{spug_version} && tar xf /tmp/{tar_gz_file} -C /tmp/{spug_version}/ '
command += f'&& rm -rf {action["dst"]} && mv /tmp/{spug_version}/{sd_dst} {action["dst"]} '
command += f'&& rm -rf /tmp/{spug_version}* && echo "transfer completed"'
else:
command = f'cd /tmp ; {action["data"]}'
helper.remote(host.id, ssh, command)
helper.send_step(h_id, 100, f'\r\n{human_time()} ** 发布成功 **')
@ -400,7 +400,6 @@ class Helper:
if env:
env = dict(env.items())
env.update(os.environ)
command = 'set -e\n' + command
task = subprocess.Popen(command, env=env, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
while True:
message = task.stdout.readline()
@ -416,3 +415,8 @@ class Helper:
self.send_info(key, out)
if code != 0:
self.send_error(key, f'exit code: {code}')
def remote_raw(self, key, ssh, command):
code, out = ssh.exec_command_raw(command)
if code != 0:
self.send_error(key, f'exit code: {code}')

View File

@ -124,10 +124,10 @@ class RequestDetailView(View):
if not req:
return json_response(error='未找到指定发布申请')
hosts = Host.objects.filter(id__in=json.loads(req.host_ids))
outputs = {x.id: {'id': x.id, 'title': x.name, 'data': [f'{human_time()} 读取数据... ']} for x in hosts}
outputs = {x.id: {'id': x.id, 'title': x.name, 'data': f'{human_time()} 读取数据... '} for x in hosts}
response = {'outputs': outputs, 'status': req.status}
if req.deploy.extend == '2':
outputs['local'] = {'id': 'local', 'data': [f'{human_time()} 读取数据... ']}
outputs['local'] = {'id': 'local', 'data': f'{human_time()} 读取数据... '}
response['s_actions'] = json.loads(req.deploy.extend_obj.server_actions)
response['h_actions'] = json.loads(req.deploy.extend_obj.host_actions)
if not response['h_actions']:
@ -139,7 +139,7 @@ class RequestDetailView(View):
for item in data:
item = json.loads(item.decode())
if 'data' in item:
outputs[item['key']]['data'].append(item['data'])
outputs[item['key']]['data'] += item['data']
if 'step' in item:
outputs[item['key']]['step'] = item['step']
if 'status' in item:
@ -160,7 +160,7 @@ class RequestDetailView(View):
return json_response(error='该申请单当前状态还不能执行发布')
hosts = Host.objects.filter(id__in=json.loads(req.host_ids))
message = f'{human_time()} 等待调度... '
outputs = {x.id: {'id': x.id, 'title': x.name, 'step': 0, 'data': [message]} for x in hosts}
outputs = {x.id: {'id': x.id, 'title': x.name, 'step': 0, 'data': message} for x in hosts}
req.status = '2'
req.do_at = human_datetime()
req.do_by = request.user
@ -168,7 +168,7 @@ class RequestDetailView(View):
Thread(target=dispatch, args=(req,)).start()
if req.deploy.extend == '2':
message = f'{human_time()} 建立连接... '
outputs['local'] = {'id': 'local', 'step': 0, 'data': [message]}
outputs['local'] = {'id': 'local', 'step': 0, 'data': message}
s_actions = json.loads(req.deploy.extend_obj.server_actions)
h_actions = json.loads(req.deploy.extend_obj.host_actions)
if not h_actions:

View File

@ -39,7 +39,7 @@ class Job:
def run(self):
if not self.token:
with self.ssh:
return self.ssh.exec_command_raw(self.command)
return self.ssh.exec_command(self.command)
self.send('\x1b[36m### Executing ...\x1b[0m\r')
code = -1
try:

View File

@ -24,9 +24,9 @@ class Host(models.Model, ModelMixin):
def private_key(self):
return self.pkey or AppSetting.get('private_key')
def get_ssh(self, pkey=None):
def get_ssh(self, pkey=None, default_env=None):
pkey = pkey or self.private_key
return SSH(self.hostname, self.port, self.username, pkey)
return SSH(self.hostname, self.port, self.username, pkey, default_env=default_env)
def to_view(self):
tmp = self.to_dict()

View File

@ -189,7 +189,8 @@ def fetch_host_extend(ssh):
"fdisk -l 2> /dev/null | grep '^Disk /' | awk '{print $5}'",
"fdisk -l 2> /dev/null | grep '^磁盘 /' | awk '{print $4}' | awk -F'' '{print $2}'"
]
code, out = ssh.exec_command(';'.join(commands))
with ssh:
code, out = ssh.exec_command_raw(';'.join(commands))
if code != 0:
raise Exception(out)
response = {'disk': [], 'public_ip_address': [], 'private_ip_address': []}
@ -252,11 +253,11 @@ def _sync_host_extend(host, private_key=None, public_key=None, password=None, ss
def _get_ssh(kwargs, pkey=None, private_key=None, public_key=None, password=None):
try:
ssh = SSH(pkey=pkey or private_key, **kwargs)
ssh.ping()
ssh.get_client()
return ssh
except AuthenticationException as e:
if password:
ssh = SSH(password=str(password), **kwargs)
ssh.add_public_key(public_key)
with SSH(password=str(password), **kwargs) as ssh:
ssh.add_public_key(public_key)
return _get_ssh(kwargs, private_key)
raise e

View File

@ -56,8 +56,8 @@ def ping_check(addr):
def host_executor(host, command):
try:
cli = host.get_ssh()
exit_code, out = cli.exec_command(command)
with host.get_ssh() as ssh:
exit_code, out = ssh.exec_command(command)
if exit_code == 0:
return True, out or '检测状态正常'
else:

View File

@ -28,8 +28,8 @@ def local_executor(command):
def host_executor(host, command):
code, out, now = 1, None, time.time()
try:
cli = host.get_ssh()
code, out = cli.exec_command(command)
with host.get_ssh() as ssh:
code, out = ssh.exec_command(command)
except AuthenticationException:
out = 'ssh authentication fail'
except socket.error as e:

View File

@ -10,13 +10,15 @@ import re
class SSH:
def __init__(self, hostname, port=22, username='root', pkey=None, password=None, connect_timeout=10):
def __init__(self, hostname, port=22, username='root', pkey=None, password=None, default_env=None,
connect_timeout=10):
self.stdout = None
self.client = None
self.channel = None
self.sftp = None
self.eof = 'Spug EOF 2108111926'
self.regex = re.compile(r'Spug EOF 2108111926 \d+[\r\n]?$')
self.default_env = self._make_env_command(default_env)
self.regex = re.compile(r'Spug EOF 2108111926 -?\d+[\r\n]?$')
self.arguments = {
'hostname': hostname,
'port': port,
@ -36,22 +38,20 @@ class SSH:
def get_client(self):
if self.client is not None:
return self.client
print('\n~~ ssh start ~~')
self.client = SSHClient()
self.client.set_missing_host_key_policy(AutoAddPolicy)
self.client.connect(**self.arguments)
return self.client
def ping(self):
self.get_client()
return True
def add_public_key(self, public_key):
command = f'mkdir -p -m 700 ~/.ssh && \
echo {public_key!r} >> ~/.ssh/authorized_keys && \
chmod 600 ~/.ssh/authorized_keys'
_, out, _ = self.client.exec_command(command)
if out.channel.recv_exit_status() != 0:
exit_code, out = self.exec_command_raw(command)
if exit_code != 0:
raise Exception(f'add public key error: {out}')
def exec_command_raw(self, command):
@ -69,7 +69,7 @@ class SSH:
channel.send(command)
out, exit_code = '', -1
for line in self.stdout:
if line.startswith(self.eof):
if self.regex.search(line):
exit_code = int(line.rsplit()[-1])
break
out += line
@ -91,10 +91,6 @@ class SSH:
yield exit_code, line
yield exit_code, line
def get_file(self, file):
sftp = self._get_sftp()
return sftp.open(file)
def put_file(self, local_path, remote_path):
sftp = self._get_sftp()
sftp.put(local_path, remote_path)
@ -115,13 +111,17 @@ class SSH:
if self.channel:
return self.channel
counter, data = 0, ''
counter = 0
self.channel = self.client.invoke_shell()
self.channel.send(b'export PS1= && stty -echo && echo Spug execute start\n')
command = 'export PS1= && stty -echo'
if self.default_env:
command += f' && {self.default_env}'
command += f' && echo {self.eof} $?\n'
self.channel.send(command.encode())
while True:
if self.channel.recv_ready():
data += self.channel.recv(8196).decode()
if 'Spug execute start\r\n' in data:
line = self.channel.recv(8196).decode()
if self.regex.search(line):
self.stdout = self.channel.makefile('r')
break
elif counter >= 100:
@ -168,6 +168,5 @@ class SSH:
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print('close √')
self.client.close()
self.client = None

View File

@ -3,9 +3,9 @@
* Copyright (c) <spug.dev@gmail.com>
* Released under the AGPL-3.0 License.
*/
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import { observer, useLocalStore } from 'mobx-react';
import { Card, Progress, Modal, Collapse, Steps } from 'antd';
import { Card, Progress, Modal, Collapse, Steps, Skeleton } from 'antd';
import { ShrinkOutlined, CaretRightOutlined, LoadingOutlined, CloseOutlined } from '@ant-design/icons';
import OutView from './OutView';
import { http, X_TOKEN } from 'libs';
@ -14,6 +14,8 @@ import store from './store';
function Ext1Console(props) {
const outputs = useLocalStore(() => ({}));
const terms = useLocalStore(() => ({}));
const [fetching, setFetching] = useState(true);
useEffect(props.request.mode === 'read' ? readDeploy : doDeploy, [])
@ -22,6 +24,7 @@ function Ext1Console(props) {
http.get(`/api/deploy/request/${props.request.id}/`)
.then(res => {
Object.assign(outputs, res.outputs)
setTimeout(() => setFetching(false), 100)
if (res.status === '2') {
socket = _makeSocket()
}
@ -34,6 +37,7 @@ function Ext1Console(props) {
http.post(`/api/deploy/request/${props.request.id}/`)
.then(res => {
Object.assign(outputs, res.outputs)
setTimeout(() => setFetching(false), 100)
socket = _makeSocket()
store.fetchRecords()
})
@ -52,7 +56,10 @@ function Ext1Console(props) {
} else {
index += 1;
const {key, data, step, status} = JSON.parse(e.data);
if (data !== undefined) outputs[key].data.push(data);
if (data !== undefined) {
outputs[key].data += data
if (terms[key]) terms[key].write(data)
}
if (step !== undefined) outputs[key].step = step;
if (status !== undefined) outputs[key].status = status;
}
@ -73,6 +80,13 @@ function Ext1Console(props) {
store.tabModes[props.request.id] = !value
}
function handleSetTerm(term, key) {
if (outputs[key] && outputs[key].data) {
term.write(outputs[key].data)
}
terms[key] = term
}
return store.tabModes[props.request.id] ? (
<Card
className={styles.item}
@ -103,29 +117,30 @@ function Ext1Console(props) {
<ShrinkOutlined/>
</div>
]}>
<Collapse
defaultActiveKey={'0'}
className={styles.collapse}
expandIcon={({isActive}) => <CaretRightOutlined style={{fontSize: 16}} rotate={isActive ? 90 : 0}/>}>
{Object.values(outputs).map((item, index) => (
<Collapse.Panel
key={index}
header={
<div className={styles.header}>
<b className={styles.title}>{item.title}</b>
<Steps size="small" className={styles.step} current={item.step} status={item.status}>
<StepItem title="等待调度" item={item} step={0}/>
<StepItem title="数据准备" item={item} step={1}/>
<StepItem title="发布前任务" item={item} step={2}/>
<StepItem title="执行发布" item={item} step={3}/>
<StepItem title="发布后任务" item={item} step={4}/>
</Steps>
</div>}>
<OutView records={item.data}/>
</Collapse.Panel>
))}
</Collapse>
<Skeleton loading={fetching} active>
<Collapse
defaultActiveKey="0"
className={styles.collapse}
expandIcon={({isActive}) => <CaretRightOutlined style={{fontSize: 16}} rotate={isActive ? 90 : 0}/>}>
{Object.entries(outputs).map(([key, item], index) => (
<Collapse.Panel
key={index}
header={
<div className={styles.header}>
<b className={styles.title}>{item.title}</b>
<Steps size="small" className={styles.step} current={item.step} status={item.status}>
<StepItem title="等待调度" item={item} step={0}/>
<StepItem title="数据准备" item={item} step={1}/>
<StepItem title="发布前任务" item={item} step={2}/>
<StepItem title="执行发布" item={item} step={3}/>
<StepItem title="发布后任务" item={item} step={4}/>
</Steps>
</div>}>
<OutView setTerm={term => handleSetTerm(term, key)}/>
</Collapse.Panel>
))}
</Collapse>
</Skeleton>
</Modal>
)
}

View File

@ -5,7 +5,7 @@
*/
import React, { useEffect, useState } from 'react';
import { observer, useLocalStore } from 'mobx-react';
import { Card, Progress, Modal, Collapse, Steps } from 'antd';
import { Card, Progress, Modal, Collapse, Steps, Skeleton } from 'antd';
import { ShrinkOutlined, CaretRightOutlined, LoadingOutlined, CloseOutlined } from '@ant-design/icons';
import OutView from './OutView';
import { http, X_TOKEN } from 'libs';
@ -13,9 +13,11 @@ import styles from './index.module.less';
import store from './store';
function Ext2Console(props) {
const terms = useLocalStore(() => ({}));
const outputs = useLocalStore(() => ({local: {id: 'local'}}));
const [sActions, setSActions] = useState([]);
const [hActions, setHActions] = useState([]);
const [fetching, setFetching] = useState(true);
useEffect(props.request.mode === 'read' ? readDeploy : doDeploy, [])
@ -25,7 +27,8 @@ function Ext2Console(props) {
.then(res => {
setSActions(res.s_actions);
setHActions(res.h_actions);
Object.assign(outputs, res.outputs)
Object.assign(outputs, res.outputs);
setTimeout(() => setFetching(false), 100)
if (res.status === '2') {
socket = _makeSocket()
}
@ -40,6 +43,7 @@ function Ext2Console(props) {
setSActions(res.s_actions);
setHActions(res.h_actions);
Object.assign(outputs, res.outputs)
setTimeout(() => setFetching(false), 100)
socket = _makeSocket()
})
return () => socket && socket.close()
@ -57,7 +61,10 @@ function Ext2Console(props) {
} else {
index += 1;
const {key, data, step, status} = JSON.parse(e.data);
if (data !== undefined) outputs[key].data.push(data);
if (data !== undefined) {
outputs[key].data += data
if (terms[key]) terms[key].write(data)
}
if (step !== undefined) outputs[key].step = step;
if (status !== undefined) outputs[key].status = status;
}
@ -80,6 +87,13 @@ function Ext2Console(props) {
store.tabModes[props.request.id] = !value
}
function handleSetTerm(term, key) {
if (outputs[key] && outputs[key].data) {
term.write(outputs[key].data)
}
terms[key] = term
}
const hostOutputs = Object.values(outputs).filter(x => x.id !== 'local');
return store.tabModes[props.request.id] ? (
<Card
@ -113,45 +127,48 @@ function Ext2Console(props) {
<ShrinkOutlined/>
</div>
]}>
<Collapse defaultActiveKey="0" className={styles.collapse}>
<Collapse.Panel header={(
<div className={styles.header}>
<b className={styles.title}/>
<Steps size="small" className={styles.step} current={outputs.local.step} status={outputs.local.status}>
<StepItem title="建立连接" item={outputs.local} step={0}/>
{sActions.map((item, index) => (
<StepItem key={index} title={item.title} item={outputs.local} step={index + 1}/>
))}
</Steps>
</div>
)}>
<OutView records={outputs.local.data}/>
</Collapse.Panel>
</Collapse>
{hostOutputs.length > 0 && (
<Collapse
defaultActiveKey="0"
className={styles.collapse}
style={{marginTop: 24}}
expandIcon={({isActive}) => <CaretRightOutlined style={{fontSize: 16}} rotate={isActive ? 90 : 0}/>}>
{hostOutputs.map((item, index) => (
<Collapse.Panel
key={index}
header={
<div className={styles.header}>
<b className={styles.title}>{item.title}</b>
<Steps size="small" className={styles.step} current={item.step} status={item.status}>
<StepItem title="等待调度" item={item} step={0}/>
{hActions.map((action, index) => (
<StepItem key={index} title={action.title} item={item} step={index + 1}/>
))}
</Steps>
</div>}>
<OutView records={item.data}/>
</Collapse.Panel>
))}
<Skeleton loading={fetching} active>
<Collapse defaultActiveKey={['0']} className={styles.collapse}>
<Collapse.Panel header={(
<div className={styles.header}>
<b className={styles.title}/>
<Steps size="small" className={styles.step} current={outputs.local.step} status={outputs.local.status}>
<StepItem title="建立连接" item={outputs.local} step={0}/>
{sActions.map((item, index) => (
<StepItem key={index} title={item.title} item={outputs.local} step={index + 1}/>
))}
</Steps>
</div>
)}>
<OutView setTerm={term => handleSetTerm(term, 'local')}/>
</Collapse.Panel>
</Collapse>
)}
{hostOutputs.length > 0 && (
<Collapse
accordion
defaultActiveKey="0"
className={styles.collapse}
style={{marginTop: 24}}
expandIcon={({isActive}) => <CaretRightOutlined style={{fontSize: 16}} rotate={isActive ? 90 : 0}/>}>
{hostOutputs.map((item, index) => (
<Collapse.Panel
key={index}
header={
<div className={styles.header}>
<b className={styles.title}>{item.title}</b>
<Steps size="small" className={styles.step} current={item.step} status={item.status}>
<StepItem title="等待调度" item={item} step={0}/>
{hActions.map((action, index) => (
<StepItem key={index} title={action.title} item={item} step={index + 1}/>
))}
</Steps>
</div>}>
<OutView setTerm={term => handleSetTerm(term, item.id)}/>
</Collapse.Panel>
))}
</Collapse>
)}
</Skeleton>
</Modal>
)
}

View File

@ -3,18 +3,28 @@
* Copyright (c) <spug.dev@gmail.com>
* Released under the AGPL-3.0 License.
*/
import React, { useRef, useEffect } from 'react';
import styles from './index.module.less';
import React, { useEffect, useRef } from 'react';
import { FitAddon } from 'xterm-addon-fit';
import { Terminal } from 'xterm';
function OutView(props) {
const el = useRef()
useEffect(() => {
if (el) el.current.scrollTop = el.current.scrollHeight
})
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)
props.setTerm(term)
fitPlugin.fit()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return (
<pre ref={el} className={styles.out}>{props.records}</pre>
<div style={{padding: '8px 0 0 15px'}}>
<div ref={el} style={{height: 300}}/>
</div>
)
}

View File

@ -92,12 +92,6 @@
color: #1890ff;
}
}
.out {
min-height: 40px;
max-height: 400px;
padding: 10px 15px;
}
}
.collapse :global(.ant-collapse-content-box) {

View File

@ -18,17 +18,16 @@ function OutView(props) {
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()
}
term.fit = () => fitPlugin.fit()
props.setTerm(term)
fitPlugin.fit()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return (
<div ref={el} style={{padding: '10px 15px'}}/>
<div style={{padding: '8px 0 0 15px'}}>
<div ref={el}/>
</div>
)
}