/** * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug * Copyright (c) * Released under the AGPL-3.0 License. */ import React, { useEffect, useRef, useState } from 'react'; import { observer, useLocalStore } from 'mobx-react'; import { Tooltip, Modal, Spin, Card } from 'antd'; import { LoadingOutlined, CheckCircleOutlined, ExclamationCircleOutlined, CodeOutlined, StopOutlined, ShrinkOutlined, ClockCircleOutlined, CloseOutlined, } from '@ant-design/icons'; import { FitAddon } from 'xterm-addon-fit'; import { Terminal } from 'xterm'; import styles from './console.module.less'; import { clsNames, http, X_TOKEN } from 'libs'; import store from './store'; import gStore from 'gStore'; import lds from 'lodash'; let gCurrent; function Console(props) { const el = useRef() const outputs = useLocalStore(() => ({})); const [term] = useState(new Terminal()); const [fitPlugin] = useState(new FitAddon()); const [token, setToken] = useState(); const [current, setCurrent] = useState(); const [sides, setSides] = useState([]); const [miniMode, setMiniMode] = useState(false); const [loading, setLoading] = useState(false); const [fetching, setFetching] = useState(true); useEffect(props.request.mode === 'read' ? readDeploy : doDeploy, []) useEffect(() => { gCurrent = current term.setOption('disableStdin', true) term.setOption('fontSize', 14) term.setOption('lineHeight', 1.2) term.setOption('fontFamily', gStore.terminal.fontFamily) term.setOption('theme', {background: '#2b2b2b', foreground: '#A9B7C6', cursor: '#2b2b2b'}) term.attachCustomKeyEventHandler((arg) => { if (arg.ctrlKey && arg.code === 'KeyC' && arg.type === 'keydown') { document.execCommand('copy') return false } return true }) term.loadAddon(fitPlugin) term.open(el.current) fitPlugin.fit() // term.write('\x1b[36m### WebSocket connecting ...\x1b[0m') const resize = () => fitPlugin.fit(); window.addEventListener('resize', resize) return () => window.removeEventListener('resize', resize); // eslint-disable-next-line react-hooks/exhaustive-deps }, []) function readDeploy() { let socket; http.get(`/api/deploy/request/${props.request.id}/`) .then(res => { _handleResponse(res) if (res.status === '2') { socket = _makeSocket(res.index) } }) return () => socket && socket.close() } function doDeploy() { let socket; http.post(`/api/deploy/request/${props.request.id}/`, {mode: props.request.mode}) .then(res => { _handleResponse(res) socket = _makeSocket() store.fetchInfo(props.request.id) }) return () => socket && socket.close() } function _handleResponse(res) { Object.assign(outputs, res.outputs) let tmp = Object.values(res.outputs).map(x => lds.pick(x, ['id', 'title'])) tmp = lds.reverse(lds.sortBy(tmp, [x => String(x.id)])) setToken(res.token) setSides(tmp) setTimeout(() => { setFetching(false) handleSwitch(tmp[0]?.id) }, 100) } function _makeSocket(index = 0) { const token = props.request.id; const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const socket = new WebSocket(`${protocol}//${window.location.host}/api/ws/request/${token}/?x-token=${X_TOKEN}`); socket.onopen = () => socket.send(String(index)); socket.onmessage = e => { if (e.data === 'pong') { socket.send(String(index)) } else { index += 1; const {key, data, status} = JSON.parse(e.data); if (!outputs[key]) return if (!lds.isNil(data)) { outputs[key].data += data if (key === gCurrent) term.write(data) } if (!lds.isNil(status)) outputs[key].status = status; } } socket.onerror = () => { for (let key of Object.keys(store.outputs)) { if (outputs[key].status === -2) { outputs[key].status = -1 } outputs[key].data += '\r\n\x1b[31mWebsocket connection failed!\x1b[0m' term.write('\r\n\x1b[31mWebsocket connection failed!\x1b[0m') } } return socket } function handleSwitch(key) { if (key === current) return setCurrent(key) gCurrent = key term.reset() term.write(outputs[key].data) } function handleTerminate() { setLoading(true) http.post('/api/exec/terminate/', {token, target: current}) .finally(() => setLoading(false)) } function openTerminal() { window.open(`/ssh?id=${current}`) } const cItem = outputs[current] || {} const localTitle = props.request.app_extend === '2' ? '本地动作' : '构建' return ( {miniMode && ( setMiniMode(false)}>
{props.request.name}
store.showConsole(props.request, true)}/>
{sides.map(item => (
handleSwitch(item.id)}> {outputs[item.id]?.status === 'error' ? ( ) : outputs[item.id]?.status === 'success' ? ( ) : outputs[item.id]?.status === 'doing' ? ( ) : ( )} {item.id === 'local' ? (
{localTitle}
) : (
{item.title}
)}
))}
)} store.showConsole(props.request, true)} title={[ {props.request.name},
setMiniMode(true)}>
]}>
任务列表
{sides.map(item => (
handleSwitch(item.id)}> {outputs[item.id]?.status === 'error' ? ( ) : outputs[item.id]?.status === 'success' ? ( ) : outputs[item.id]?.status === 'doing' ? ( ) : ( )} {item.id === 'local' ? (
{localTitle}
) : (
{item.title}
)}
))}
{cItem.id === 'local' ? localTitle : cItem.title}
{loading ? ( ) : ( {cItem.status === 'doing' ? ( ) : ( )} )} openTerminal(current)}/>
) } export default observer(Console)