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'])
def disconnect(self, code):
self.chan.close()
self.ssh.close()
# print('Connection close')
if self.chan:
self.chan.close()
if self.ssh:
self.ssh.close()
def connect(self):
if has_host_perm(self.user, self.id):

View File

@ -5,13 +5,18 @@
*/
import React, { useEffect, useState } from '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 {
FolderOutlined,
FolderOpenOutlined,
CloudServerOutlined,
SearchOutlined,
SyncOutlined
SyncOutlined,
CopyOutlined,
ReloadOutlined,
VerticalAlignBottomOutlined,
VerticalAlignMiddleOutlined,
CloseOutlined
} from '@ant-design/icons';
import { NotFound, AuthButton } from 'components';
import Terminal from './Terminal';
@ -85,11 +90,17 @@ function WebSSH(props) {
.finally(() => setFetching(false))
}
function _openNode(node) {
node.vId = String(new Date().getTime())
hosts.push(node);
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(node.vId)
setActiveId(newNode.vId)
}
function handleSelect(e) {
@ -98,12 +109,13 @@ function WebSSH(props) {
}
}
function handleRemove(key, action) {
if (action === 'remove') {
const index = lds.findIndex(hosts, x => x.vId === key);
if (index !== -1) {
hosts.splice(index, 1);
setHosts(lds.cloneDeep(hosts));
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) {
@ -111,7 +123,22 @@ function WebSSH(props) {
} 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
}
}
@ -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 =
' __ __ _ __\n' +
' _____ ____ __ __ ____ _ _ __ ___ / /_ / /_ ___ _____ ____ ___ (_)____ ____ _ / /\n' +
@ -158,12 +222,14 @@ function WebSSH(props) {
<Input allowClear className={styles.search} prefix={<SearchOutlined style={{color: '#999'}}/>}
placeholder="输入检索" onChange={e => setSearchValue(e.target.value)}/>
<Button icon={<SyncOutlined/>} type="link" loading={fetching} onClick={fetchNodes}/>
<Tree.DirectoryTree
defaultExpandAll
expandAction="doubleClick"
treeData={treeData}
icon={renderIcon}
onSelect={(k, e) => handleSelect(e)}/>
{treeData.length > 0 ? (
<Tree.DirectoryTree
defaultExpandAll
expandAction="doubleClick"
treeData={treeData}
icon={renderIcon}
onSelect={(k, e) => handleSelect(e)}/>
) : null}
</Spin>
</div>
<div className={styles.split} onMouseDown={e => posX = e.pageX}/>
@ -174,17 +240,21 @@ function WebSSH(props) {
activeKey={activeId}
type="editable-card"
onTabClick={key => setActiveId(key)}
onEdit={handleRemove}
onEdit={(key, action) => action === 'remove' ? handleRemove(key, 'self') : null}
style={{width: `calc(100vw - ${width}px)`}}
tabBarExtraContent={<AuthButton
auth="host.console.list"
type="primary"
disabled={!activeId}
style={{marginRight: 5}}
onClick={handleOpenFileManager}
icon={<FolderOpenOutlined/>}>文件管理器</AuthButton>}>
tabBarExtraContent={hosts.length === 0 ? (
<div className={styles.tips}>小提示双击标签快速复制窗口右击标签展开更多操作</div>
) : (
<AuthButton
auth="host.console.list"
type="primary"
disabled={!activeId}
style={{marginRight: 5}}
onClick={handleOpenFileManager}
icon={<FolderOpenOutlined/>}>文件管理器</AuthButton>
)}>
{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}/>
</Tabs.TabPane>
))}

View File

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