mirror of https://github.com/openspug/spug
A 新增web终端自定义主题
parent
dfbe1bf426
commit
02261a7a6f
|
@ -2,6 +2,7 @@
|
|||
# Copyright: (c) <spug.dev@gmail.com>
|
||||
# Released under the AGPL-3.0 License.
|
||||
from django.db import models
|
||||
from apps.account.models import User
|
||||
from libs import ModelMixin
|
||||
import json
|
||||
|
||||
|
@ -40,3 +41,13 @@ class Setting(models.Model, ModelMixin):
|
|||
|
||||
class Meta:
|
||||
db_table = 'settings'
|
||||
|
||||
|
||||
class UserSetting(models.Model, ModelMixin):
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
key = models.CharField(max_length=32)
|
||||
value = models.TextField()
|
||||
|
||||
class Meta:
|
||||
db_table = 'user_settings'
|
||||
unique_together = ('user', 'key')
|
||||
|
|
|
@ -3,10 +3,12 @@
|
|||
# Released under the AGPL-3.0 License.
|
||||
# from django.urls import path
|
||||
from django.conf.urls import url
|
||||
from .views import *
|
||||
from apps.setting.views import *
|
||||
from apps.setting.user import UserSettingView
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^$', SettingView.as_view()),
|
||||
url(r'^user/$', UserSettingView.as_view()),
|
||||
url(r'^ldap_test/$', ldap_test),
|
||||
url(r'^email_test/$', email_test),
|
||||
url(r'^mfa/$', MFAView.as_view()),
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
|
||||
# Copyright: (c) <spug.dev@gmail.com>
|
||||
# Released under the AGPL-3.0 License.
|
||||
from django.views.generic import View
|
||||
from libs import JsonParser, Argument, json_response
|
||||
from apps.setting.models import UserSetting
|
||||
|
||||
|
||||
class UserSettingView(View):
|
||||
def get(self, request):
|
||||
response = {}
|
||||
for item in UserSetting.objects.filter(user=request.user):
|
||||
response[item.key] = item.value
|
||||
return json_response(response)
|
||||
|
||||
def post(self, request):
|
||||
form, error = JsonParser(
|
||||
Argument('key', help='参数错误'),
|
||||
Argument('value', help='参数错误'),
|
||||
).parse(request.body)
|
||||
if error is None:
|
||||
UserSetting.objects.update_or_create(
|
||||
user=request.user,
|
||||
key=form.key,
|
||||
defaults={'value': form.value}
|
||||
)
|
||||
return self.get(request)
|
||||
return json_response(error=error)
|
|
@ -0,0 +1,45 @@
|
|||
/**
|
||||
* Copyright (c) OpenSpug Organization. https://github.com/openspug/spug
|
||||
* Copyright (c) <spug.dev@gmail.com>
|
||||
* Released under the AGPL-3.0 License.
|
||||
*/
|
||||
import { observable } from 'mobx';
|
||||
import http from 'libs/http';
|
||||
import themes from 'pages/ssh/themes';
|
||||
|
||||
class Store {
|
||||
isReady = false;
|
||||
@observable terminal = {
|
||||
fontSize: 16,
|
||||
fontFamily: 'Courier',
|
||||
theme: 'dark',
|
||||
styles: themes['dark']
|
||||
};
|
||||
|
||||
_handleSettings = (res) => {
|
||||
if (res.terminal) {
|
||||
const terminal = JSON.parse(res.terminal)
|
||||
terminal.styles = themes[terminal.theme]
|
||||
this.terminal = terminal
|
||||
}
|
||||
}
|
||||
|
||||
fetchUserSettings = () => {
|
||||
if (this.isReady) return
|
||||
http.get('/api/setting/user/')
|
||||
.then(res => {
|
||||
this.isReady = true
|
||||
this._handleSettings(res)
|
||||
})
|
||||
};
|
||||
|
||||
updateUserSettings = (key, value) => {
|
||||
return http.post('/api/setting/user/', {key, value})
|
||||
.then(res => {
|
||||
this.isReady = true
|
||||
this._handleSettings(res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default new Store()
|
|
@ -229,6 +229,7 @@ class FileManager extends React.Component {
|
|||
<Input ref={ref => this.input2 = ref} size="small" className={styles.input}
|
||||
suffix={<div style={{color: '#999', fontSize: 12}}>回车确认</div>}
|
||||
value={this.state.inputPath} onChange={e => this.setState({inputPath: e.target.value})}
|
||||
onBlur={this.handleInputEnter}
|
||||
onPressEnter={this.handleInputEnter}/>
|
||||
) : (
|
||||
<Breadcrumb className={styles.bread}>
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
/**
|
||||
* Copyright (c) OpenSpug Organization. https://github.com/openspug/spug
|
||||
* Copyright (c) <spug.dev@gmail.com>
|
||||
* Released under the AGPL-3.0 License.
|
||||
*/
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Drawer, Form, Button, Select, Space, message } from 'antd';
|
||||
import themes from './themes';
|
||||
import gStore from 'gStore';
|
||||
import css from './setting.module.less'
|
||||
|
||||
function Setting(props) {
|
||||
const [theme, setTheme] = useState('dark')
|
||||
const [styles, setStyles] = useState(themes['dark'])
|
||||
const [fontSize, setFontSize] = useState(14)
|
||||
const [fontFamily, setFontFamily] = useState('Courier')
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const {theme, styles, fontSize, fontFamily} = gStore.terminal
|
||||
setTheme(theme)
|
||||
setStyles(styles)
|
||||
setFontSize(fontSize)
|
||||
setFontFamily(fontFamily)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [gStore.terminal])
|
||||
|
||||
useEffect(() => {
|
||||
setStyles(themes[theme])
|
||||
}, [theme])
|
||||
|
||||
function handleSubmit() {
|
||||
setLoading(true)
|
||||
const data = {fontSize, fontFamily, theme}
|
||||
gStore.updateUserSettings('terminal', JSON.stringify(data))
|
||||
.then(() => {
|
||||
message.success('已保存')
|
||||
props.onClose()
|
||||
})
|
||||
.finally(() => setLoading(false))
|
||||
}
|
||||
|
||||
return (<Drawer
|
||||
title="终端设置"
|
||||
placement="right"
|
||||
width={300}
|
||||
visible={props.visible}
|
||||
onClose={props.onClose}>
|
||||
<Form layout="vertical">
|
||||
<Form.Item label="字体大小">
|
||||
<Select value={fontSize} placeholder="请选择字体大小" onChange={v => setFontSize(v)}>
|
||||
<Select.Option value={12}>12</Select.Option>
|
||||
<Select.Option value={14}>14</Select.Option>
|
||||
<Select.Option value={16}>16</Select.Option>
|
||||
<Select.Option value={18}>18</Select.Option>
|
||||
<Select.Option value={20}>20</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="字体名称">
|
||||
<Select value={fontFamily} placeholder="请选择字体" onChange={v => setFontFamily(v)}>
|
||||
<Select.Option value="Courier">Courier</Select.Option>
|
||||
<Select.Option value="Consolas">Consolas</Select.Option>
|
||||
<Select.Option value="DejaVu Sans Mono">DejaVu Sans Mono</Select.Option>
|
||||
<Select.Option value="Droid Sans Mono">Droid Sans Mono</Select.Option>
|
||||
<Select.Option value="Monaco">Monaco</Select.Option>
|
||||
<Select.Option value="Menlo">Menlo</Select.Option>
|
||||
<Select.Option value="monospace">monospace</Select.Option>
|
||||
<Select.Option value="Source Code Pro">Source Code Pro</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="主题配色">
|
||||
<Space wrap className={css.theme} size={12}>
|
||||
{Object.entries(themes).map(([key, item]) => (
|
||||
<pre key={key} style={{background: item.background, color: item.foreground}}
|
||||
onClick={() => setTheme(key)}>spug</pre>))}
|
||||
</Space>
|
||||
</Form.Item>
|
||||
<Form.Item label="预览">
|
||||
<div className={css.preview}
|
||||
style={{fontSize, fontFamily, background: styles.background, color: styles.foreground}}>
|
||||
<div>Welcome to Spug !</div>
|
||||
<div>* Website: https://spug.cc</div>
|
||||
<div>[root@iZ8vb48roZ ~]# ls</div>
|
||||
<div>
|
||||
<span style={{color: styles.brightBlue}}>apps </span>
|
||||
<span style={{color: styles.brightRed}}>bak.tar.gz </span>
|
||||
<span style={{color: styles.brightGreen}}>manage.py </span>
|
||||
<span>README.md</span>
|
||||
</div>
|
||||
<div>[root@iZ8vb48roZ ~]# pwd</div>
|
||||
<div>/data/api</div>
|
||||
<div>[root@iZ8vb48roZ ~]#</div>
|
||||
</div>
|
||||
</Form.Item>
|
||||
<Button block type="primary" className={css.btn} loading={loading} onClick={handleSubmit}>保存</Button>
|
||||
</Form>
|
||||
</Drawer>)
|
||||
}
|
||||
|
||||
export default Setting
|
|
@ -9,7 +9,7 @@ import { FitAddon } from 'xterm-addon-fit';
|
|||
import { X_TOKEN } from 'libs';
|
||||
import 'xterm/css/xterm.css';
|
||||
import styles from './index.module.less';
|
||||
|
||||
import gStore from 'gStore';
|
||||
|
||||
function WebSSH(props) {
|
||||
const container = useRef();
|
||||
|
@ -18,8 +18,9 @@ function WebSSH(props) {
|
|||
|
||||
useEffect(() => {
|
||||
term.loadAddon(fitPlugin);
|
||||
term.setOption('fontFamily', 'Source Code Pro, Courier New, Courier, Monaco, monospace, PingFang SC, Microsoft YaHei')
|
||||
term.setOption('theme', {background: '#2b2b2b', foreground: '#A9B7C6'})
|
||||
term.setOption('fontSize', gStore.terminal.fontSize)
|
||||
term.setOption('fontFamily', gStore.terminal.fontFamily)
|
||||
term.setOption('theme', gStore.terminal.styles)
|
||||
term.attachCustomKeyEventHandler((arg) => {
|
||||
if (arg.code === 'PageUp' && arg.type === 'keydown') {
|
||||
term.scrollPages(-1)
|
||||
|
@ -58,6 +59,14 @@ function WebSSH(props) {
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
term.setOption('fontSize', gStore.terminal.fontSize)
|
||||
term.setOption('fontFamily', gStore.terminal.fontFamily)
|
||||
term.setOption('theme', gStore.terminal.styles)
|
||||
fitTerminal()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [gStore.terminal])
|
||||
|
||||
useEffect(() => {
|
||||
if (props.vId === props.activeId) {
|
||||
setTimeout(() => term.focus())
|
||||
|
@ -79,9 +88,7 @@ function WebSSH(props) {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className={styles.termContainer}>
|
||||
<div className={styles.terminal} ref={container}/>
|
||||
</div>
|
||||
<div className={styles.terminal} ref={container}/>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -18,11 +18,14 @@ import {
|
|||
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';
|
||||
|
@ -31,6 +34,7 @@ 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([]);
|
||||
|
@ -46,6 +50,7 @@ function WebSSH(props) {
|
|||
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
|
||||
}, [])
|
||||
|
@ -222,11 +227,11 @@ function WebSSH(props) {
|
|||
<div className={styles.hosts}>
|
||||
<Spin spinning={fetching}>
|
||||
<Input allowClear className={styles.search} prefix={<SearchOutlined style={{color: '#999'}}/>}
|
||||
placeholder="输入检索" onChange={e => setSearchValue(e.target.value)}/>
|
||||
placeholder="输入主机名/IP检索" onChange={e => setSearchValue(e.target.value)}/>
|
||||
<Button icon={<SyncOutlined/>} type="link" loading={fetching} onClick={fetchNodes}/>
|
||||
{treeData.length > 0 ? (
|
||||
<Tree.DirectoryTree
|
||||
defaultExpandAll={treeData.length > 0 && treeData < 5}
|
||||
defaultExpandAll={treeData.length > 0 && treeData.length < 5}
|
||||
expandAction="doubleClick"
|
||||
treeData={treeData}
|
||||
icon={renderIcon}
|
||||
|
@ -247,13 +252,15 @@ function WebSSH(props) {
|
|||
tabBarExtraContent={hosts.length === 0 ? (
|
||||
<div className={styles.tips}>小提示:双击标签快速复制窗口,右击标签展开更多操作。</div>
|
||||
) : sshMode ? (
|
||||
<AuthButton
|
||||
auth="host.console.list"
|
||||
type="link"
|
||||
disabled={!activeId}
|
||||
style={{marginRight: 5}}
|
||||
onClick={handleOpenFileManager}
|
||||
icon={<LeftOutlined/>}>文件管理器</AuthButton>
|
||||
<React.Fragment>
|
||||
<AuthButton
|
||||
auth="host.console.list"
|
||||
type="link"
|
||||
disabled={!activeId}
|
||||
onClick={handleOpenFileManager}
|
||||
icon={<LeftOutlined/>}>文件管理器</AuthButton>
|
||||
<SkinFilled className={styles.setting} onClick={() => setVisible2(true)}/>
|
||||
</React.Fragment>
|
||||
) : null}>
|
||||
{hosts.map(item => (
|
||||
<Tabs.TabPane key={item.vId} tab={<TabRender host={item}/>}>
|
||||
|
@ -280,6 +287,7 @@ function WebSSH(props) {
|
|||
onClose={() => setVisible(false)}>
|
||||
<FileManager id={hostId}/>
|
||||
</Drawer>
|
||||
<Setting visible={visible2} onClose={() => setVisible2(false)}/>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{height: '100vh'}}>
|
||||
|
|
|
@ -100,6 +100,13 @@
|
|||
height: calc(100vh - 66px);
|
||||
}
|
||||
|
||||
.setting {
|
||||
cursor: pointer;
|
||||
padding-right: 6px;
|
||||
margin-right: 6px;
|
||||
color: #fa8c16;
|
||||
}
|
||||
|
||||
:global(.ant-tabs-nav) {
|
||||
height: 42px;
|
||||
margin: 0;
|
||||
|
@ -126,14 +133,16 @@
|
|||
}
|
||||
}
|
||||
|
||||
.termContainer {
|
||||
.terminal {
|
||||
margin: 12px;
|
||||
border-radius: 6px;
|
||||
background-color: #2b2b2b;
|
||||
padding: 10px 0 10px 10px;
|
||||
|
||||
.terminal {
|
||||
height: calc(100vh - 84px);
|
||||
:global(.xterm) {
|
||||
padding: 10px 0 6px 10px;
|
||||
height: calc(100vh - 66px);
|
||||
}
|
||||
|
||||
:global(.xterm-viewport) {
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
.theme {
|
||||
|
||||
pre {
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #333333;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.preview {
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
font-family: Source Code Pro, Courier New, Courier, Monaco, monospace, PingFang SC, Microsoft YaHei;
|
||||
}
|
||||
|
||||
.btn {
|
||||
margin-top: 24px;
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
export default {
|
||||
solarized_dark: {
|
||||
foreground: '#839496', background: '#2b2b2b', cursor: '#839496',
|
||||
|
||||
black: '#1b1b1b', brightBlack: '#626262',
|
||||
|
||||
red: '#bb5653', brightRed: '#bb5653',
|
||||
|
||||
green: '#909d62', brightGreen: '#909d62',
|
||||
|
||||
yellow: '#eac179', brightYellow: '#eac179',
|
||||
|
||||
blue: '#7da9c7', brightBlue: '#7da9c7',
|
||||
|
||||
magenta: '#b06597', brightMagenta: '#b06597',
|
||||
|
||||
cyan: '#8cdcd8', brightCyan: '#8cdcd8',
|
||||
|
||||
white: '#d8d8d8', brightWhite: '#f7f7f7'
|
||||
}, dark: {
|
||||
foreground: '#c7c7c7', background: '#000000', cursor: '#c7c7c7',
|
||||
|
||||
black: '#000000', brightBlack: '#676767',
|
||||
|
||||
red: '#c91b00', brightRed: '#ff6d67',
|
||||
|
||||
green: '#00c200', brightGreen: '#5ff967',
|
||||
|
||||
yellow: '#c7c400', brightYellow: '#fefb67',
|
||||
|
||||
blue: '#0225c7', brightBlue: '#6871ff',
|
||||
|
||||
magenta: '#c930c7', brightMagenta: '#ff76ff',
|
||||
|
||||
cyan: '#00c5c7', brightCyan: '#5ffdff',
|
||||
|
||||
white: '#c7c7c7', brightWhite: '#fffefe'
|
||||
}, ubuntu: {
|
||||
foreground: '#f1f1ef', background: '#3f0e2f', cursor: '#c7c7c7',
|
||||
|
||||
black: '#3c4345', brightBlack: '#676965',
|
||||
|
||||
red: '#d71e00', brightRed: '#f44135',
|
||||
|
||||
green: '#5da602', brightGreen: '#98e342',
|
||||
|
||||
yellow: '#cfad00', brightYellow: '#fcea60',
|
||||
|
||||
blue: '#417ab3', brightBlue: '#83afd8',
|
||||
|
||||
magenta: '#88658d', brightMagenta: '#bc93b6',
|
||||
|
||||
cyan: '#00a7aa', brightCyan: '#37e5e7',
|
||||
|
||||
white: '#dbded8', brightWhite: '#f1f1ef'
|
||||
}, light: {
|
||||
foreground: '#000000', background: '#fffefe', cursor: '#000000',
|
||||
|
||||
black: '#000000', brightBlack: '#676767',
|
||||
|
||||
red: '#c91b00', brightRed: '#ff6d67',
|
||||
|
||||
green: '#00c200', brightGreen: '#5ff967',
|
||||
|
||||
yellow: '#c7c400', brightYellow: '#fefb67',
|
||||
|
||||
blue: '#0225c7', brightBlue: '#6871ff',
|
||||
|
||||
magenta: '#c930c7', brightMagenta: '#ff76ff',
|
||||
|
||||
cyan: '#00c5c7', brightCyan: '#5ffdff',
|
||||
|
||||
white: '#c7c7c7', brightWhite: '#fffefe'
|
||||
}, solarized_light: {
|
||||
foreground: '#657b83', background: '#fdf6e3', cursor: '#657b83',
|
||||
|
||||
black: '#073642', brightBlack: '#002b36',
|
||||
|
||||
red: '#dc322f', brightRed: '#cb4b16',
|
||||
|
||||
green: '#859900', brightGreen: '#586e75',
|
||||
|
||||
yellow: '#b58900', brightYellow: '#657b83',
|
||||
|
||||
blue: '#268bd2', brightBlue: '#839496',
|
||||
|
||||
magenta: '#d33682', brightMagenta: '#6c71c4',
|
||||
|
||||
cyan: '#2aa198', brightCyan: '#93a1a1',
|
||||
|
||||
white: '#eee8d5', brightWhite: '#fdf6e3'
|
||||
}, material: {
|
||||
foreground: '#2e2d2c', background: '#eeeeee', cursor: '#2e2d2c',
|
||||
|
||||
black: '#2c2c2c', brightBlack: '#535353',
|
||||
|
||||
red: '#c52728', brightRed: '#ee524f',
|
||||
|
||||
green: '#558a2f', brightGreen: '#8bc24a',
|
||||
|
||||
yellow: '#f8a725', brightYellow: '#ffea3b',
|
||||
|
||||
blue: '#1564bf', brightBlue: '#64b4f5',
|
||||
|
||||
magenta: '#691e99', brightMagenta: '#b967c7',
|
||||
|
||||
cyan: '#00828e', brightCyan: '#26c5d9',
|
||||
|
||||
white: '#f2f1f1', brightWhite: '#e0dfdf'
|
||||
},
|
||||
}
|
Loading…
Reference in New Issue