A web terminal添加标签菜单 #464

pull/467/head
vapao 2022-03-31 16:27:52 +08:00
parent e026ce09bf
commit 8eec8532d4
3 changed files with 116 additions and 31 deletions

View File

@ -107,9 +107,10 @@ class SSHConsumer(WebsocketConsumer):
self.chan.send(data['data']) self.chan.send(data['data'])
def disconnect(self, code): def disconnect(self, code):
self.chan.close() if self.chan:
self.ssh.close() self.chan.close()
# print('Connection close') if self.ssh:
self.ssh.close()
def connect(self): def connect(self):
if has_host_perm(self.user, self.id): if has_host_perm(self.user, self.id):

View File

@ -5,13 +5,18 @@
*/ */
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { Tabs, Tree, Input, Spin, Button } from 'antd'; import { Tabs, Tree, Input, Spin, Dropdown, Menu, Button } from 'antd';
import { import {
FolderOutlined, FolderOutlined,
FolderOpenOutlined, FolderOpenOutlined,
CloudServerOutlined, CloudServerOutlined,
SearchOutlined, SearchOutlined,
SyncOutlined SyncOutlined,
CopyOutlined,
ReloadOutlined,
VerticalAlignBottomOutlined,
VerticalAlignMiddleOutlined,
CloseOutlined
} from '@ant-design/icons'; } from '@ant-design/icons';
import { NotFound, AuthButton } from 'components'; import { NotFound, AuthButton } from 'components';
import Terminal from './Terminal'; import Terminal from './Terminal';
@ -85,11 +90,17 @@ function WebSSH(props) {
.finally(() => setFetching(false)) .finally(() => setFetching(false))
} }
function _openNode(node) { function _openNode(node, replace) {
node.vId = String(new Date().getTime()) const newNode = {...node}
hosts.push(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)) setHosts(lds.cloneDeep(hosts))
setActiveId(node.vId) setActiveId(newNode.vId)
} }
function handleSelect(e) { function handleSelect(e) {
@ -98,12 +109,13 @@ function WebSSH(props) {
} }
} }
function handleRemove(key, action) { function handleRemove(key, target) {
if (action === 'remove') { const index = lds.findIndex(hosts, x => x.vId === key);
const index = lds.findIndex(hosts, x => x.vId === key); if (index === -1) return;
if (index !== -1) { switch (target) {
hosts.splice(index, 1); case 'self':
setHosts(lds.cloneDeep(hosts)); hosts.splice(index, 1)
setHosts([...hosts])
if (hosts.length > index) { if (hosts.length > index) {
setActiveId(hosts[index].vId) setActiveId(hosts[index].vId)
} else if (hosts.length) { } else if (hosts.length) {
@ -111,7 +123,22 @@ function WebSSH(props) {
} else { } else {
setActiveId(undefined) 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
} }
} }
@ -139,6 +166,43 @@ function WebSSH(props) {
} }
} }
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 (
<Dropdown trigger={['contextMenu']} overlay={(
<Menu onClick={({key, domEvent}) => handeTabAction(key, host, domEvent)}>
<Menu.Item key="copy" icon={<CopyOutlined/>}>复制窗口</Menu.Item>
<Menu.Item key="reconnect" icon={<ReloadOutlined/>}>重新连接</Menu.Item>
<Menu.Item key="rClose"
icon={<VerticalAlignBottomOutlined style={{transform: 'rotate(90deg)'}}/>}>关闭右侧</Menu.Item>
<Menu.Item key="oClose"
icon={<VerticalAlignMiddleOutlined style={{transform: 'rotate(90deg)'}}/>}>关闭其他</Menu.Item>
<Menu.Item key="aClose" icon={<CloseOutlined/>}>关闭所有</Menu.Item>
</Menu>
)}>
<div className={styles.tabRender} onDoubleClick={() => handeTabAction('copy', host)}>{host.title}</div>
</Dropdown>
)
}
const spug_web_terminal = const spug_web_terminal =
' __ __ _ __\n' + ' __ __ _ __\n' +
' _____ ____ __ __ ____ _ _ __ ___ / /_ / /_ ___ _____ ____ ___ (_)____ ____ _ / /\n' + ' _____ ____ __ __ ____ _ _ __ ___ / /_ / /_ ___ _____ ____ ___ (_)____ ____ _ / /\n' +
@ -158,12 +222,14 @@ function WebSSH(props) {
<Input allowClear className={styles.search} prefix={<SearchOutlined style={{color: '#999'}}/>} <Input allowClear className={styles.search} prefix={<SearchOutlined style={{color: '#999'}}/>}
placeholder="输入检索" onChange={e => setSearchValue(e.target.value)}/> placeholder="输入检索" onChange={e => setSearchValue(e.target.value)}/>
<Button icon={<SyncOutlined/>} type="link" loading={fetching} onClick={fetchNodes}/> <Button icon={<SyncOutlined/>} type="link" loading={fetching} onClick={fetchNodes}/>
<Tree.DirectoryTree {treeData.length > 0 ? (
defaultExpandAll <Tree.DirectoryTree
expandAction="doubleClick" defaultExpandAll
treeData={treeData} expandAction="doubleClick"
icon={renderIcon} treeData={treeData}
onSelect={(k, e) => handleSelect(e)}/> icon={renderIcon}
onSelect={(k, e) => handleSelect(e)}/>
) : null}
</Spin> </Spin>
</div> </div>
<div className={styles.split} onMouseDown={e => posX = e.pageX}/> <div className={styles.split} onMouseDown={e => posX = e.pageX}/>
@ -174,17 +240,21 @@ function WebSSH(props) {
activeKey={activeId} activeKey={activeId}
type="editable-card" type="editable-card"
onTabClick={key => setActiveId(key)} onTabClick={key => setActiveId(key)}
onEdit={handleRemove} onEdit={(key, action) => action === 'remove' ? handleRemove(key, 'self') : null}
style={{width: `calc(100vw - ${width}px)`}} style={{width: `calc(100vw - ${width}px)`}}
tabBarExtraContent={<AuthButton tabBarExtraContent={hosts.length === 0 ? (
auth="host.console.list" <div className={styles.tips}>小提示双击标签快速复制窗口右击标签展开更多操作</div>
type="primary" ) : (
disabled={!activeId} <AuthButton
style={{marginRight: 5}} auth="host.console.list"
onClick={handleOpenFileManager} type="primary"
icon={<FolderOpenOutlined/>}>文件管理器</AuthButton>}> disabled={!activeId}
style={{marginRight: 5}}
onClick={handleOpenFileManager}
icon={<FolderOpenOutlined/>}>文件管理器</AuthButton>
)}>
{hosts.map(item => ( {hosts.map(item => (
<Tabs.TabPane key={item.vId} tab={item.title}> <Tabs.TabPane key={item.vId} tab={<TabRender host={item}/>}>
<Terminal id={item.id} vId={item.vId} activeId={activeId}/> <Terminal id={item.id} vId={item.vId} activeId={activeId}/>
</Tabs.TabPane> </Tabs.TabPane>
))} ))}

View File

@ -50,6 +50,14 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.tips {
position: absolute;
top: 12px;
left: 12px;
font-size: 12px;
color: #666;
}
:global(.ant-tabs-nav) { :global(.ant-tabs-nav) {
height: 42px; height: 42px;
margin: 0; margin: 0;
@ -89,3 +97,9 @@
align-items: center; align-items: center;
margin-bottom: 15px; margin-bottom: 15px;
} }
.tabRender {
user-select: none;
padding: 8px 8px 8px 16px;
margin: 0 -8px 0 -16px;
}