diff --git a/spug_api/apps/setting/models.py b/spug_api/apps/setting/models.py index cdab32f..58e90b0 100644 --- a/spug_api/apps/setting/models.py +++ b/spug_api/apps/setting/models.py @@ -2,6 +2,7 @@ # Copyright: (c) # 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') diff --git a/spug_api/apps/setting/urls.py b/spug_api/apps/setting/urls.py index 3ac1c94..89c6de5 100644 --- a/spug_api/apps/setting/urls.py +++ b/spug_api/apps/setting/urls.py @@ -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()), diff --git a/spug_api/apps/setting/user.py b/spug_api/apps/setting/user.py new file mode 100644 index 0000000..2ffb455 --- /dev/null +++ b/spug_api/apps/setting/user.py @@ -0,0 +1,28 @@ +# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug +# Copyright: (c) +# 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) diff --git a/spug_web/src/gStore.js b/spug_web/src/gStore.js new file mode 100644 index 0000000..cc52340 --- /dev/null +++ b/spug_web/src/gStore.js @@ -0,0 +1,45 @@ +/** + * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug + * Copyright (c) + * 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() \ No newline at end of file diff --git a/spug_web/src/pages/ssh/FileManager.js b/spug_web/src/pages/ssh/FileManager.js index 7290b64..a7abd53 100644 --- a/spug_web/src/pages/ssh/FileManager.js +++ b/spug_web/src/pages/ssh/FileManager.js @@ -229,6 +229,7 @@ class FileManager extends React.Component { this.input2 = ref} size="small" className={styles.input} suffix={
回车确认
} value={this.state.inputPath} onChange={e => this.setState({inputPath: e.target.value})} + onBlur={this.handleInputEnter} onPressEnter={this.handleInputEnter}/> ) : ( diff --git a/spug_web/src/pages/ssh/Setting.js b/spug_web/src/pages/ssh/Setting.js new file mode 100644 index 0000000..17526be --- /dev/null +++ b/spug_web/src/pages/ssh/Setting.js @@ -0,0 +1,100 @@ +/** + * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug + * Copyright (c) + * 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 ( +
+ + + + + + + + + {Object.entries(themes).map(([key, item]) => ( +
 setTheme(key)}>spug
))} +
+
+ +
+
Welcome to Spug !
+
* Website: https://spug.cc
+
[root@iZ8vb48roZ ~]# ls
+
+ apps + bak.tar.gz + manage.py + README.md +
+
[root@iZ8vb48roZ ~]# pwd
+
/data/api
+
[root@iZ8vb48roZ ~]#
+
+
+ +
+
) +} + +export default Setting \ No newline at end of file diff --git a/spug_web/src/pages/ssh/Terminal.js b/spug_web/src/pages/ssh/Terminal.js index 539e349..d6d70bf 100644 --- a/spug_web/src/pages/ssh/Terminal.js +++ b/spug_web/src/pages/ssh/Terminal.js @@ -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 ( -
-
-
+
) } diff --git a/spug_web/src/pages/ssh/index.js b/spug_web/src/pages/ssh/index.js index 3302fbe..df5525d 100644 --- a/spug_web/src/pages/ssh/index.js +++ b/spug_web/src/pages/ssh/index.js @@ -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) {
} - placeholder="输入检索" onChange={e => setSearchValue(e.target.value)}/> + placeholder="输入主机名/IP检索" onChange={e => setSearchValue(e.target.value)}/>
) : (
diff --git a/spug_web/src/pages/ssh/index.module.less b/spug_web/src/pages/ssh/index.module.less index 9f9eccb..a80a10b 100644 --- a/spug_web/src/pages/ssh/index.module.less +++ b/spug_web/src/pages/ssh/index.module.less @@ -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; } } diff --git a/spug_web/src/pages/ssh/setting.module.less b/spug_web/src/pages/ssh/setting.module.less new file mode 100644 index 0000000..bcf23d9 --- /dev/null +++ b/spug_web/src/pages/ssh/setting.module.less @@ -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; +} \ No newline at end of file diff --git a/spug_web/src/pages/ssh/themes.js b/spug_web/src/pages/ssh/themes.js new file mode 100644 index 0000000..c00aa15 --- /dev/null +++ b/spug_web/src/pages/ssh/themes.js @@ -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' + }, +} \ No newline at end of file