/** * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug * Copyright (c) * Released under the AGPL-3.0 License. */ import React, { useEffect, useState } from 'react'; import { observer } from 'mobx-react'; import { Tabs, Tree, Input, Spin, Dropdown, Menu, Button, Drawer } from 'antd'; import { FolderOutlined, FolderOpenOutlined, CloudServerOutlined, SearchOutlined, SyncOutlined, CopyOutlined, ReloadOutlined, VerticalAlignBottomOutlined, VerticalAlignMiddleOutlined, CloseOutlined, LeftOutlined, SkinFilled, } from '@ant-design/icons'; import { NotFound, AuthButton } from 'components'; import Terminal from './Terminal'; import FileManager from './FileManager'; import Setting from './Setting'; import { http, hasPermission, includes } from 'libs'; import gStore from 'gStore'; import styles from './index.module.less'; import LogoSpugText from 'layout/logo-spug-white.png'; import lds from 'lodash'; let posX = 0 function WebSSH(props) { const [visible, setVisible] = useState(false); const [visible2, setVisible2] = useState(false); const [fetching, setFetching] = useState(true); const [rawTreeData, setRawTreeData] = useState([]); const [rawHostList, setRawHostList] = useState([]); const [treeData, setTreeData] = useState([]); const [searchValue, setSearchValue] = useState(); const [hosts, setHosts] = useState([]); const [activeId, setActiveId] = useState(); const [hostId, setHostId] = useState(); const [width, setWidth] = useState(280); const [sshMode] = useState(hasPermission('host.console.view')) useEffect(() => { window.document.title = 'Spug web terminal' window.addEventListener('beforeunload', leaveTips) fetchNodes() gStore.fetchUserSettings() return () => window.removeEventListener('beforeunload', leaveTips) // eslint-disable-next-line react-hooks/exhaustive-deps }, []) useEffect(() => { if (searchValue) { const newTreeData = rawHostList.filter(x => includes([x.title, x.hostname], searchValue)) setTreeData(newTreeData) } else { setTreeData(rawTreeData) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [searchValue]) function leaveTips(e) { e.returnValue = '确定要离开页面?' } function fetchNodes() { setFetching(true) http.get('/api/host/group/?with_hosts=1') .then(res => { const tmp = {} setRawTreeData(res.treeData) setTreeData(res.treeData) const loop = (data) => { for (let item of data) { if (item.children) { loop(item.children) } else if (item.isLeaf) { tmp[item.id] = item } } } loop(res.treeData) setRawHostList(Object.values(tmp)) const query = new URLSearchParams(props.location.search); const id = query.get('id'); if (id) { const node = lds.find(Object.values(tmp), {id: Number(id)}) if (node) _openNode(node) } }) .finally(() => setFetching(false)) } function _openNode(node, replace) { const newNode = {...node} newNode.vId = String(new Date().getTime()) if (replace) { const index = lds.findIndex(hosts, {vId: node.vId}) if (index >= 0) hosts[index] = newNode } else { hosts.push(newNode); } setHosts(lds.cloneDeep(hosts)) setActiveId(newNode.vId) } function handleSelect(e) { if (e.nativeEvent.detail > 1 && e.node.isLeaf) { _openNode(e.node) } } function handleRemove(key, target) { const index = lds.findIndex(hosts, x => x.vId === key); if (index === -1) return; switch (target) { case 'self': hosts.splice(index, 1) setHosts([...hosts]) if (hosts.length > index) { setActiveId(hosts[index].vId) } else if (hosts.length) { setActiveId(hosts[index - 1].vId) } else { setActiveId(undefined) } break case 'right': hosts.splice(index + 1, hosts.length) setHosts([...hosts]) setActiveId(key) break case 'other': setHosts([hosts[index]]) setActiveId(key) break case 'all': setHosts([]) setActiveId(undefined) break default: break } } function handleOpenFileManager() { const index = lds.findIndex(hosts, x => x.vId === activeId); if (index !== -1) { setHostId(hosts[index].id) setVisible(true) } } function renderIcon(node) { if (node.isLeaf) { return } else if (node.expanded) { return } else { return } } function handleMouseMove(e) { if (posX) { setWidth(e.pageX); } } function handeTabAction(action, host, e) { if (e) e.stopPropagation() switch (action) { case 'copy': return _openNode(host) case 'reconnect': return _openNode(host, true) case 'rClose': return handleRemove(host.vId, 'right') case 'oClose': return handleRemove(host.vId, 'other') case 'aClose': return handleRemove(host.vId, 'all') default: break } } function TabRender(props) { const host = props.host; return ( handeTabAction(key, host, domEvent)}> }>复制窗口 }>重新连接 }>关闭右侧 }>关闭其他 }>关闭所有 )}>
handeTabAction('copy', host)}>{host.title}
) } const spug_web_terminal = ' __ __ _ __\n' + ' _____ ____ __ __ ____ _ _ __ ___ / /_ / /_ ___ _____ ____ ___ (_)____ ____ _ / /\n' + ' / ___// __ \\ / / / // __ `/ | | /| / // _ \\ / __ \\ / __// _ \\ / ___// __ `__ \\ / // __ \\ / __ `// / \n' + ' (__ )/ /_/ // /_/ // /_/ / | |/ |/ // __// /_/ / / /_ / __// / / / / / / // // / / // /_/ // / \n' + '/____// .___/ \\__,_/ \\__, / |__/|__/ \\___//_.___/ \\__/ \\___//_/ /_/ /_/ /_//_//_/ /_/ \\__,_//_/ \n' + ' /_/ /____/ \n' return hasPermission('host.console.view|host.console.list') ? (
posX = 0} onMouseMove={handleMouseMove}>
logo
} placeholder="输入主机名/IP检索" onChange={e => setSearchValue(e.target.value)}/>
posX = e.pageX}/>
setActiveId(key)} onEdit={(key, action) => action === 'remove' ? handleRemove(key, 'self') : null} style={{background: '#fff', width: `calc(100vw - ${width}px)`}} tabBarExtraContent={hosts.length === 0 ? (
小提示:双击标签快速复制窗口,右击标签展开更多操作。
) : sshMode ? ( }>文件管理器 setVisible2(true)}/> ) : null}> {hosts.map(item => ( }> {sshMode ? ( ) : (
)}
))}
{hosts.length === 0 && (
{spug_web_terminal}
)}
setVisible(false)}> setVisible2(false)}/>
) : (
) } export default observer(WebSSH)