mirror of
https://github.com/sipeed/NanoKVM.git
synced 2026-09-11 00:22:56 -05:00
feat: support reboot
perf: update menu bar draggable bounds perf: unify logout logic and styles in settings
This commit is contained in:
@@ -53,16 +53,16 @@ jwt:
|
||||
revokeTokensOnLogout: true
|
||||
|
||||
# Address for custom STUN server
|
||||
# Note: You can disable the STUN service by setting it to 'disable' (e.g., in a LAN environment)
|
||||
stun: stun.l.google.com:19302
|
||||
|
||||
# Address and authentication for custom TURN server
|
||||
turn:
|
||||
turnAddr: turn.cloudflare.com:3478
|
||||
turnAddr: example_addr
|
||||
turnUser: example_user
|
||||
turnCred: example_cred
|
||||
```
|
||||
|
||||
|
||||
## Compile & Deploy
|
||||
|
||||
Note: Use Linux operating system (x86-64). This build process is not compatible with ARM, Windows or macOS.
|
||||
|
||||
@@ -50,10 +50,12 @@ jwt:
|
||||
# 在帐号登出时是否使所有 jwt token 失效。默认为 true
|
||||
revokeTokensOnLogout: true
|
||||
|
||||
# 自定义 STUN 服务器的地址
|
||||
# 注意:可以设置为“disable”来禁用 STUN 服务(例如在局域网环境中使用时)
|
||||
stun: stun.l.google.com:19302
|
||||
|
||||
turn:
|
||||
turnAddr: turn.cloudflare.com:3478
|
||||
turnAddr: example_addr
|
||||
turnUser: example_user
|
||||
turnCred: example_cred
|
||||
```
|
||||
|
||||
@@ -43,4 +43,6 @@ func vmRouter(r *gin.Engine) {
|
||||
api.GET("/vm/mdns", service.GetMdnsState) // get mDNS state
|
||||
api.POST("/vm/mdns/enable", service.EnableMdns) // enable mDNS
|
||||
api.POST("/vm/mdns/disable", service.DisableMdns) // disable mDNS
|
||||
|
||||
api.POST("/vm/system/reboot", service.Reboot) // reboot system
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ func (s *Service) DisableMdns(c *gin.Context) {
|
||||
}
|
||||
|
||||
_ = os.Remove(AvahiDaemonPid)
|
||||
_ = os.Remove(AvahiDaemonScript)
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debugf("avahi-daemon stopped")
|
||||
|
||||
25
server/service/vm/system.go
Normal file
25
server/service/vm/system.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package vm
|
||||
|
||||
import (
|
||||
"NanoKVM-Server/proto"
|
||||
"os/exec"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (s *Service) Reboot(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
log.Println("reboot system...")
|
||||
|
||||
err := exec.Command("reboot").Run()
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -1, "operation failed")
|
||||
log.Errorf("failed to reboot: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debug("system rebooted")
|
||||
}
|
||||
@@ -91,3 +91,8 @@ export function enableMdns() {
|
||||
export function disableMdns() {
|
||||
return http.post('/api/vm/mdns/disable');
|
||||
}
|
||||
|
||||
// reboot
|
||||
export function reboot() {
|
||||
return http.post('/api/vm/system/reboot');
|
||||
}
|
||||
|
||||
@@ -207,7 +207,11 @@ const en = {
|
||||
disk: 'Virtual Disk',
|
||||
diskDesc: 'Mount virtual U-disk on the remote host',
|
||||
network: 'Virtual Network',
|
||||
networkDesc: 'Mount virtual network card on the remote host'
|
||||
networkDesc: 'Mount virtual network card on the remote host',
|
||||
reboot: 'Reboot',
|
||||
rebootDesc: 'Are you sure you want to reboot NanoKVM?',
|
||||
okBtn: 'Yes',
|
||||
cancelBtn: 'No'
|
||||
},
|
||||
tailscale: {
|
||||
title: 'Tailscale',
|
||||
@@ -241,7 +245,7 @@ const en = {
|
||||
deviceIP: 'Device IP',
|
||||
account: 'Account',
|
||||
logout: 'Logout',
|
||||
logout2: 'Confirm logout',
|
||||
logoutDesc: 'Are you sure you want to logout?',
|
||||
uninstall: 'Uninstall Tailscale',
|
||||
okBtn: 'Yes',
|
||||
cancelBtn: 'No'
|
||||
@@ -261,7 +265,10 @@ const en = {
|
||||
webAccount: 'Web Account Name',
|
||||
password: 'Password',
|
||||
updateBtn: 'Change',
|
||||
logoutBtn: 'Logout'
|
||||
logoutBtn: 'Logout',
|
||||
logoutDesc: 'Are you sure you want to logout?',
|
||||
okBtn: 'Yes',
|
||||
cancelBtn: 'No'
|
||||
}
|
||||
},
|
||||
error: {
|
||||
|
||||
@@ -233,7 +233,7 @@ const zh = {
|
||||
deviceIP: '设备地址',
|
||||
account: '账号',
|
||||
logout: '退出',
|
||||
logout2: '确认退出?',
|
||||
logoutDesc: '确定要退出吗?',
|
||||
uninstall: '卸载 Tailscale',
|
||||
okBtn: '确认',
|
||||
cancelBtn: '取消'
|
||||
@@ -253,8 +253,22 @@ const zh = {
|
||||
webAccount: '网页帐号',
|
||||
password: '密码',
|
||||
updateBtn: '修改',
|
||||
logoutBtn: '退出'
|
||||
logoutBtn: '退出',
|
||||
logoutDesc: '确定要退出吗?',
|
||||
confirm: '确定',
|
||||
cancel: '取消'
|
||||
}
|
||||
},
|
||||
error: {
|
||||
title: '我们遇到了问题',
|
||||
refresh: '刷新'
|
||||
},
|
||||
fullscreen: {
|
||||
toggle: '切换全屏'
|
||||
},
|
||||
menu: {
|
||||
collapse: '收起',
|
||||
expand: '展开'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -26,16 +26,45 @@ export const Menu = () => {
|
||||
const [menuDisabledItems, setMenuDisabledItems] = useAtom(menuDisabledItemsAtom);
|
||||
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(true);
|
||||
const [bounds, setBounds] = useState({ left: 0, right: 0, top: 0, bottom: 0 });
|
||||
|
||||
const nodeRef = useRef<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// disabled menu items
|
||||
const items = getMenuDisabledItems();
|
||||
setMenuDisabledItems(items);
|
||||
|
||||
// react-draggable bounds
|
||||
const handleResize = () => {
|
||||
if (!nodeRef.current) return;
|
||||
|
||||
const elementRect = nodeRef.current.getBoundingClientRect();
|
||||
const width = (window.innerWidth - elementRect.width) / 2;
|
||||
|
||||
setBounds({
|
||||
left: -width,
|
||||
top: -10,
|
||||
right: width,
|
||||
bottom: window.innerHeight - elementRect.height - 10
|
||||
});
|
||||
};
|
||||
|
||||
handleResize();
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Draggable nodeRef={nodeRef} handle="strong" positionOffset={{ x: '-50%', y: '0%' }}>
|
||||
<Draggable
|
||||
nodeRef={nodeRef}
|
||||
bounds={bounds}
|
||||
handle="strong"
|
||||
positionOffset={{ x: '-50%', y: '0%' }}
|
||||
>
|
||||
<div ref={nodeRef} className="fixed left-1/2 top-[10px] z-[1000] -translate-x-1/2">
|
||||
<div className="sticky top-[10px] flex w-full justify-center">
|
||||
<div
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { LogoutOutlined } from '@ant-design/icons';
|
||||
import { Button, Divider } from 'antd';
|
||||
import { Divider } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import * as api from '@/api/auth.ts';
|
||||
import { removeToken } from '@/lib/cookie.ts';
|
||||
|
||||
import { Logout } from './logout.tsx';
|
||||
|
||||
export const Account = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -25,18 +25,6 @@ export const Account = () => {
|
||||
navigate('/auth/password');
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
api.logout().then((rsp) => {
|
||||
if (rsp.code !== 0) {
|
||||
console.log(rsp.msg);
|
||||
return;
|
||||
}
|
||||
|
||||
removeToken();
|
||||
navigate('/auth/login');
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="text-base font-bold">{t('settings.account.title')}</div>
|
||||
@@ -60,11 +48,7 @@ export const Account = () => {
|
||||
</div>
|
||||
<Divider />
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Button type="primary" danger icon={<LogoutOutlined />} onClick={logout}>
|
||||
{t('settings.account.logoutBtn')}
|
||||
</Button>
|
||||
</div>
|
||||
<Logout />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
40
web/src/pages/desktop/menu/settings/account/logout.tsx
Normal file
40
web/src/pages/desktop/menu/settings/account/logout.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { LogoutOutlined } from '@ant-design/icons';
|
||||
import { Button, Popconfirm } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import * as api from '@/api/auth.ts';
|
||||
import { removeToken } from '@/lib/cookie.ts';
|
||||
|
||||
export const Logout = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
function logout() {
|
||||
api.logout().then((rsp) => {
|
||||
if (rsp.code !== 0) {
|
||||
console.log(rsp.msg);
|
||||
return;
|
||||
}
|
||||
|
||||
removeToken();
|
||||
navigate('/auth/login');
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex justify-center pt-3">
|
||||
<Popconfirm
|
||||
placement="bottom"
|
||||
title={t('settings.account.logoutDesc')}
|
||||
okText={t('settings.account.okBtn')}
|
||||
cancelText={t('settings.account.cancelBtn')}
|
||||
onConfirm={logout}
|
||||
>
|
||||
<Button danger type="primary" size="large" shape="round" icon={<LogoutOutlined />}>
|
||||
{t('settings.account.logoutBtn')}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Mdns } from './mdns.tsx';
|
||||
import { Oled } from './oled.tsx';
|
||||
import { Reboot } from './reboot.tsx';
|
||||
import { Ssh } from './ssh.tsx';
|
||||
import { VirtualDevices } from './virtual-devices.tsx';
|
||||
import { Wifi } from './wifi.tsx';
|
||||
@@ -22,6 +23,9 @@ export const Device = () => {
|
||||
<Mdns />
|
||||
<VirtualDevices />
|
||||
</div>
|
||||
<Divider />
|
||||
|
||||
<Reboot />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
59
web/src/pages/desktop/menu/settings/device/reboot.tsx
Normal file
59
web/src/pages/desktop/menu/settings/device/reboot.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import { useState } from 'react';
|
||||
import { ReloadOutlined } from '@ant-design/icons';
|
||||
import { Button, Popconfirm } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import * as api from '@/api/vm.ts';
|
||||
|
||||
export const Reboot = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
function reboot() {
|
||||
if (isLoading) return;
|
||||
setIsLoading(true);
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 30000);
|
||||
|
||||
api
|
||||
.reboot()
|
||||
.then((rsp) => {
|
||||
if (rsp.code !== 0) {
|
||||
console.log(rsp.msg);
|
||||
setIsLoading(false);
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
setIsLoading(false);
|
||||
clearTimeout(timeoutId);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex justify-center pt-3">
|
||||
<Popconfirm
|
||||
placement="bottom"
|
||||
title={t('settings.device.rebootDesc')}
|
||||
okText={t('settings.device.okBtn')}
|
||||
cancelText={t('settings.device.cancelBtn')}
|
||||
onConfirm={reboot}
|
||||
>
|
||||
<Button
|
||||
danger
|
||||
type="primary"
|
||||
size="large"
|
||||
shape="round"
|
||||
loading={isLoading}
|
||||
icon={<ReloadOutlined />}
|
||||
>
|
||||
{t('settings.device.reboot')}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { LogoutOutlined } from '@ant-design/icons';
|
||||
import { Button, Switch } from 'antd';
|
||||
import { Button, Divider, Popconfirm, Switch } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import * as api from '@/api/extensions/tailscale.ts';
|
||||
@@ -17,7 +17,6 @@ export const Device = ({ status, onLogout }: DeviceProps) => {
|
||||
|
||||
const [isRunning, setIsRunning] = useState(false);
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
const [isConfirmation, setIsConfirmation] = useState(false);
|
||||
const [isLogging, setIsLogging] = useState(false);
|
||||
const [errMsg, setErrMsg] = useState('');
|
||||
|
||||
@@ -82,30 +81,27 @@ export const Device = ({ status, onLogout }: DeviceProps) => {
|
||||
<span>{t('settings.tailscale.account')}</span>
|
||||
<span>{status.account}</span>
|
||||
</div>
|
||||
<Divider />
|
||||
|
||||
<div className="flex justify-center pt-10">
|
||||
{!isConfirmation ? (
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
shape="round"
|
||||
icon={<LogoutOutlined />}
|
||||
onClick={() => setIsConfirmation(true)}
|
||||
>
|
||||
{t('settings.tailscale.logout')}
|
||||
</Button>
|
||||
) : (
|
||||
<div className="flex justify-center pt-3">
|
||||
<Popconfirm
|
||||
placement="bottom"
|
||||
title={t('settings.tailscale.logoutDesc')}
|
||||
okText={t('settings.tailscale.okBtn')}
|
||||
cancelText={t('settings.tailscale.cancelBtn')}
|
||||
onConfirm={logout}
|
||||
>
|
||||
<Button
|
||||
danger
|
||||
type="primary"
|
||||
size="large"
|
||||
shape="round"
|
||||
icon={<LogoutOutlined />}
|
||||
loading={isLogging}
|
||||
onClick={logout}
|
||||
>
|
||||
{t('settings.tailscale.logout2')}
|
||||
{t('settings.tailscale.logout')}
|
||||
</Button>
|
||||
)}
|
||||
</Popconfirm>
|
||||
</div>
|
||||
|
||||
{errMsg && <span className="text-red-500">{errMsg}</span>}
|
||||
|
||||
@@ -43,7 +43,13 @@ export const Install = ({ setIsLocked, onSuccess }: InstallProps) => {
|
||||
icon={<DownloadOutlined />}
|
||||
subTitle={t('settings.tailscale.notInstall')}
|
||||
extra={
|
||||
<Button type="primary" size="large" loading={state === 'installing'} onClick={install}>
|
||||
<Button
|
||||
key="install"
|
||||
type="primary"
|
||||
size="large"
|
||||
loading={state === 'installing'}
|
||||
onClick={install}
|
||||
>
|
||||
{state === 'installing'
|
||||
? t('settings.tailscale.installing')
|
||||
: t('settings.tailscale.install')}
|
||||
@@ -57,7 +63,7 @@ export const Install = ({ setIsLocked, onSuccess }: InstallProps) => {
|
||||
subTitle={t('settings.tailscale.retry')}
|
||||
icon={<InfoCircleOutlined />}
|
||||
extra={
|
||||
<Card styles={{ body: { padding: 0 } }}>
|
||||
<Card key="tips" styles={{ body: { padding: 0 } }}>
|
||||
<ul className="list-decimal text-left font-mono text-sm text-neutral-300">
|
||||
<li>
|
||||
{t('settings.tailscale.download')}
|
||||
|
||||
@@ -106,7 +106,7 @@ export const Update = ({ setIsLocked }: UpdateProps) => {
|
||||
title={`${currentVersion} -> ${latestVersion}`}
|
||||
subTitle={t('settings.update.available')}
|
||||
extra={[
|
||||
<Button type="primary" onClick={update}>
|
||||
<Button key="confirm" type="primary" onClick={update}>
|
||||
{t('settings.update.confirm')}
|
||||
</Button>
|
||||
]}
|
||||
|
||||
Reference in New Issue
Block a user