mirror of https://github.com/openspug/spug
A web terminal添加标签菜单 #464
parent
e026ce09bf
commit
8eec8532d4
|
@ -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):
|
||||||
|
if self.chan:
|
||||||
self.chan.close()
|
self.chan.close()
|
||||||
|
if self.ssh:
|
||||||
self.ssh.close()
|
self.ssh.close()
|
||||||
# print('Connection close')
|
|
||||||
|
|
||||||
def connect(self):
|
def connect(self):
|
||||||
if has_host_perm(self.user, self.id):
|
if has_host_perm(self.user, self.id):
|
||||||
|
|
|
@ -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) {
|
if (index === -1) return;
|
||||||
hosts.splice(index, 1);
|
switch (target) {
|
||||||
setHosts(lds.cloneDeep(hosts));
|
case 'self':
|
||||||
|
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}/>
|
||||||
|
{treeData.length > 0 ? (
|
||||||
<Tree.DirectoryTree
|
<Tree.DirectoryTree
|
||||||
defaultExpandAll
|
defaultExpandAll
|
||||||
expandAction="doubleClick"
|
expandAction="doubleClick"
|
||||||
treeData={treeData}
|
treeData={treeData}
|
||||||
icon={renderIcon}
|
icon={renderIcon}
|
||||||
onSelect={(k, e) => handleSelect(e)}/>
|
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 ? (
|
||||||
|
<div className={styles.tips}>小提示:双击标签快速复制窗口,右击标签展开更多操作。</div>
|
||||||
|
) : (
|
||||||
|
<AuthButton
|
||||||
auth="host.console.list"
|
auth="host.console.list"
|
||||||
type="primary"
|
type="primary"
|
||||||
disabled={!activeId}
|
disabled={!activeId}
|
||||||
style={{marginRight: 5}}
|
style={{marginRight: 5}}
|
||||||
onClick={handleOpenFileManager}
|
onClick={handleOpenFileManager}
|
||||||
icon={<FolderOpenOutlined/>}>文件管理器</AuthButton>}>
|
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>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
Loading…
Reference in New Issue