mirror of
https://github.com/sipeed/NanoKVM.git
synced 2026-09-11 00:22:56 -05:00
update menu
- refactor tailscale - update terminal icon - update virtual devices icon - rename some files
This commit is contained in:
@@ -21,17 +21,27 @@ export function deleteWolMac(mac: string) {
|
||||
});
|
||||
}
|
||||
|
||||
// check if tailscale exists
|
||||
export function getTailscale() {
|
||||
return http.get('/api/network/tailscale');
|
||||
}
|
||||
|
||||
// install tailscale
|
||||
export function installTailscale() {
|
||||
return http.post('/api/network/tailscale/install');
|
||||
}
|
||||
|
||||
// run tailscale
|
||||
export function runTailscale() {
|
||||
return http.post('/api/network/tailscale/run');
|
||||
// get tailscale status
|
||||
export function getTailscaleStatus() {
|
||||
return http.get('/api/network/tailscale/status');
|
||||
}
|
||||
|
||||
// update tailscale status
|
||||
export function updateTailscaleStatus(command: 'up' | 'down') {
|
||||
return http.post('/api/network/tailscale/status', { command });
|
||||
}
|
||||
|
||||
// login tailscale
|
||||
export function loginTailscale() {
|
||||
return http.post('/api/network/tailscale/login');
|
||||
}
|
||||
|
||||
// logout tailscale
|
||||
export function logoutTailscale() {
|
||||
return http.post('/api/network/tailscale/logout');
|
||||
}
|
||||
|
||||
@@ -142,12 +142,9 @@ const en = {
|
||||
},
|
||||
tailscale: {
|
||||
loading: 'Loading...',
|
||||
notInstalled: 'Tailscale not found! Please install.',
|
||||
notInstall: 'Tailscale not found! Please install.',
|
||||
install: 'Install',
|
||||
installing: 'Installing...',
|
||||
urlPeriod: 'This url is valid for 10 minutes',
|
||||
installed: 'Tailscale already installed. Please login.',
|
||||
login: 'Login',
|
||||
installing: 'Installing',
|
||||
failed: 'Install failed',
|
||||
retry: 'Please refresh and try again. Or try to install manually',
|
||||
download: 'Download the',
|
||||
@@ -155,7 +152,18 @@ const en = {
|
||||
unzip: 'and unzip it',
|
||||
upTailscale: 'Upload tailscale to NanoKVM directory /usr/bin/',
|
||||
upTailscaled: 'Upload tailscaled to NanoKVM directory /usr/sbin/',
|
||||
refresh: 'Refresh current page'
|
||||
refresh: 'Refresh current page',
|
||||
notLogin:
|
||||
'The device has not been bound yet. Please login and bind this device to your account.',
|
||||
urlPeriod: 'This url is valid for 10 minutes',
|
||||
login: 'Login',
|
||||
loginSuccess: 'Login Success',
|
||||
enable: 'Enable Tailscale',
|
||||
deviceName: 'Device Name',
|
||||
deviceIP: 'Device IP',
|
||||
account: 'Account',
|
||||
logout: 'Logout',
|
||||
logout2: 'Sure to logout?'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -146,12 +146,9 @@ const fr = {
|
||||
},
|
||||
tailscale: {
|
||||
loading: 'Chargement...',
|
||||
notInstalled: "Tailscale non trouvé! Veuillez l'installer.",
|
||||
notInstall: "Tailscale non trouvé! Veuillez l'installer.",
|
||||
install: 'Installer',
|
||||
installing: 'Installation...',
|
||||
urlPeriod: "L'URL est valide pendant 10 minutes",
|
||||
installed: 'Tailscale est déjà installé. Veuillez vous connecter.',
|
||||
login: 'Connexion',
|
||||
installing: 'Installation',
|
||||
failed: 'Installation échouée',
|
||||
retry: "Veuillez rafraîchir et réessayer. Ou essayez d'installer manuellement",
|
||||
download: 'Télécharger le',
|
||||
@@ -159,7 +156,17 @@ const fr = {
|
||||
unzip: 'et décompressez-le',
|
||||
upTailscale: 'Téléverser tailscale dans le répertoire NanoKVM /usr/sbin/',
|
||||
upTailscaled: 'Téléverser tailscaled dans le répertoire NanoKVM /usr/sbin/',
|
||||
refresh: 'Rafraîchir la page courante'
|
||||
refresh: 'Rafraîchir la page courante',
|
||||
notLogin: "L'appareil n'est pas relié. Connectez-vous et liez cet appareil à votre compte.",
|
||||
urlPeriod: "L'URL est valide pendant 10 minutes",
|
||||
login: 'Connexion',
|
||||
loginSuccess: 'Connexion réussie',
|
||||
enable: 'Démarrer Tailscale',
|
||||
deviceName: "Nom de l'appareil",
|
||||
deviceIP: "IP de l'appareil",
|
||||
account: 'Compte',
|
||||
logout: 'Déconnexion',
|
||||
logout2: 'Voulez-vous vous déconnecter?'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -147,12 +147,9 @@ const ru = {
|
||||
},
|
||||
tailscale: {
|
||||
loading: 'Загрузка...',
|
||||
notInstalled: 'Tailscale не найден! Пожалуйста, установите.',
|
||||
notInstall: 'Tailscale не найден! Пожалуйста, установите.',
|
||||
install: 'Установить',
|
||||
installing: 'Установка...',
|
||||
urlPeriod: 'Этот адрес действителен в течение 10 минут',
|
||||
installed: 'Tailscale уже установлен. Пожалуйста, войдите в систему.',
|
||||
login: 'Войти',
|
||||
installing: 'Установка',
|
||||
failed: 'Не удалось установить',
|
||||
retry: 'Пожалуйста, обновите и попробуйте снова, или попробуйте установить вручную',
|
||||
download: 'Скачайте',
|
||||
@@ -160,7 +157,17 @@ const ru = {
|
||||
unzip: 'и разархивируйте его',
|
||||
upTailscale: 'Переместите tailscale в каталог /usr/bin/ на NanoKVM',
|
||||
upTailscaled: 'Переместите tailscaled в каталог /usr/sbin/ на NanoKVM',
|
||||
refresh: 'Обновите текущую страницу'
|
||||
refresh: 'Обновите текущую страницу',
|
||||
notLogin: 'Устройство не привязано. Войдите, чтобы привязать его к аккаунту.',
|
||||
urlPeriod: 'Этот адрес действителен в течение 10 минут',
|
||||
login: 'Войти',
|
||||
loginSuccess: 'Вход выполнен',
|
||||
enable: 'Включить Tailscale',
|
||||
deviceName: 'Имя устройства',
|
||||
deviceIP: 'IP адрес устройства',
|
||||
account: 'аккаунт',
|
||||
logout: 'Выход',
|
||||
logout2: 'Вы действительно хотите выйти?'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -141,12 +141,9 @@ const zh = {
|
||||
},
|
||||
tailscale: {
|
||||
loading: '加载中...',
|
||||
notInstalled: '未检测到 Tailscale,请先安装',
|
||||
notInstall: '未检测到 Tailscale,请先安装',
|
||||
install: '安装',
|
||||
installing: '安装中...',
|
||||
urlPeriod: '该链接10分钟内有效',
|
||||
installed: 'Tailscale 已安装,请执行登录操作',
|
||||
login: '登录',
|
||||
installing: '安装中',
|
||||
failed: '安装失败',
|
||||
retry: '请刷新后重试,或尝试手动安装',
|
||||
download: '下载',
|
||||
@@ -154,7 +151,17 @@ const zh = {
|
||||
unzip: '并解压',
|
||||
upTailscale: '将 tailscale 文件上传到 /usr/bin/ 目录',
|
||||
upTailscaled: '将 tailscaled 文件上传到 /usr/sbin/ 目录',
|
||||
refresh: '刷新页面'
|
||||
refresh: '刷新页面',
|
||||
notLogin: '该设备尚未绑定,请点击登录并将这台设备绑定到您的账号。',
|
||||
urlPeriod: '该链接10分钟内有效',
|
||||
login: '登录',
|
||||
loginSuccess: '登录完成',
|
||||
enable: '启用 Tailscale',
|
||||
deviceName: '设备名称',
|
||||
deviceIP: '设备地址',
|
||||
account: '账号',
|
||||
logout: '退出',
|
||||
logout2: '确认退出?'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,14 +3,14 @@ import clsx from 'clsx';
|
||||
import { MenuIcon } from 'lucide-react';
|
||||
|
||||
import { Image } from './image';
|
||||
import { Keyboard } from './keyboard';
|
||||
import { Mouse } from './mouse';
|
||||
import { Power } from './power';
|
||||
import { Keyboard } from './keyboard.tsx';
|
||||
import { Mouse } from './mouse.tsx';
|
||||
import { Power } from './power.tsx';
|
||||
import { Screen } from './screen';
|
||||
import { Script } from './script';
|
||||
import { Settings } from './settings';
|
||||
import { Terminal } from './terminal';
|
||||
import { Wol } from './wol';
|
||||
import { Wol } from './wol.tsx';
|
||||
|
||||
export const MenuPhone = () => {
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
|
||||
@@ -6,12 +6,12 @@ import { SettingsIcon } from 'lucide-react';
|
||||
import { isSettingsOpenAtom } from '@/jotai/settings.ts';
|
||||
|
||||
import { About } from './about.tsx';
|
||||
import { ChangePassword } from './change-password.tsx';
|
||||
import { Language } from './language.tsx';
|
||||
import { Logout } from './logout.tsx';
|
||||
import { Tailscale } from './tailscale.tsx';
|
||||
import { Password } from './password.tsx';
|
||||
import { Tailscale } from './tailscale';
|
||||
import { Update } from './update.tsx';
|
||||
import { VirtualDevice } from './virtual-device.tsx';
|
||||
import { VirtualDevices } from './virtual-devices.tsx';
|
||||
|
||||
export const Settings = () => {
|
||||
const [isSettingsOpen, setIsSettingsOpen] = useAtom(isSettingsOpenAtom);
|
||||
@@ -22,13 +22,15 @@ export const Settings = () => {
|
||||
<Language />
|
||||
<About />
|
||||
<Update setIsBadgeVisible={setIsBadgeVisible} />
|
||||
<Divider style={{ margin: '10px 0' }} />
|
||||
<Divider style={{ margin: '5px 0' }} />
|
||||
|
||||
<VirtualDevices />
|
||||
<Divider style={{ margin: '5px 0' }} />
|
||||
|
||||
<VirtualDevice />
|
||||
<Tailscale />
|
||||
<Divider style={{ margin: '10px 0' }} />
|
||||
<Divider style={{ margin: '5px 0' }} />
|
||||
|
||||
<ChangePassword />
|
||||
<Password />
|
||||
<Logout />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
export const ChangePassword = () => {
|
||||
export const Password = () => {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -1,202 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { Button, Modal } from 'antd';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { LoaderCircleIcon } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import * as api from '@/api/network';
|
||||
import { isSettingsOpenAtom } from '@/jotai/settings.ts';
|
||||
|
||||
type State = '' | 'installing' | 'installed' | 'failed';
|
||||
|
||||
export const Tailscale = () => {
|
||||
const { t } = useTranslation();
|
||||
const setIsSettingsOpen = useSetAtom(isSettingsOpenAtom);
|
||||
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isStarting, setIsStarting] = useState(false);
|
||||
const [state, setState] = useState<State>('');
|
||||
const [loginUrl, setLoginUrl] = useState('');
|
||||
|
||||
function openModal() {
|
||||
check();
|
||||
|
||||
setIsModalOpen(true);
|
||||
setIsSettingsOpen(false);
|
||||
}
|
||||
|
||||
function check() {
|
||||
if (['installing', 'installed'].includes(state)) return;
|
||||
|
||||
if (isLoading) return;
|
||||
setIsLoading(true);
|
||||
|
||||
api
|
||||
.getTailscale()
|
||||
.then((rsp) => {
|
||||
if (rsp.code !== 0) {
|
||||
console.log(rsp.msg);
|
||||
return;
|
||||
}
|
||||
|
||||
if (rsp.data.exist) {
|
||||
setState('installed');
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
function install() {
|
||||
if (state === 'installing') return;
|
||||
setState('installing');
|
||||
|
||||
api
|
||||
.installTailscale()
|
||||
.then((rsp) => {
|
||||
if (rsp.code !== 0) {
|
||||
console.log(rsp.msg);
|
||||
setState('failed');
|
||||
return;
|
||||
}
|
||||
|
||||
setState('installed');
|
||||
})
|
||||
.catch(() => {
|
||||
setState('failed');
|
||||
});
|
||||
}
|
||||
|
||||
function run() {
|
||||
if (state !== 'installed') return;
|
||||
|
||||
if (isStarting) return;
|
||||
setIsStarting(true);
|
||||
|
||||
setLoginUrl('');
|
||||
|
||||
api
|
||||
.runTailscale()
|
||||
.then((rsp) => {
|
||||
if (rsp.code !== 0) {
|
||||
console.log(rsp.msg);
|
||||
return;
|
||||
}
|
||||
|
||||
if (rsp.data.url) {
|
||||
updateLoginUrl(rsp.data.url);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setIsStarting(false);
|
||||
});
|
||||
}
|
||||
|
||||
function updateLoginUrl(url: string) {
|
||||
setLoginUrl(url);
|
||||
|
||||
window.open(url, '_blank');
|
||||
|
||||
setTimeout(() => setLoginUrl(''), 10 * 60 * 1000);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="flex cursor-pointer select-none items-center rounded px-3 py-1.5 hover:bg-neutral-600"
|
||||
onClick={openModal}
|
||||
>
|
||||
Tailscale
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
title="Tailscale"
|
||||
open={isModalOpen}
|
||||
width={350}
|
||||
footer={null}
|
||||
centered={true}
|
||||
maskClosable={false}
|
||||
onCancel={() => setIsModalOpen(false)}
|
||||
>
|
||||
<div className="flex h-[180px] items-center justify-center">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center space-x-2 text-neutral-500">
|
||||
<LoaderCircleIcon className="animate-spin" size={18} />
|
||||
<span>{t('tailscale.loading')}</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{state === '' && (
|
||||
<div className="flex h-full flex-col items-center justify-center space-y-5">
|
||||
<span>{t('tailscale.notInstalled')}</span>
|
||||
<div className="flex justify-center">
|
||||
<Button type="primary" onClick={install}>
|
||||
{t('tailscale.install')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{state === 'installing' && (
|
||||
<div className="flex items-center justify-center space-x-2 text-neutral-500">
|
||||
<LoaderCircleIcon className="animate-spin" size={18} />
|
||||
<span>{t('tailscale.installing')}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{state === 'installed' && (
|
||||
<div className="flex h-full flex-col items-center justify-center space-y-5">
|
||||
{loginUrl ? (
|
||||
<>
|
||||
<a href={loginUrl} target="_blank">
|
||||
{loginUrl}
|
||||
</a>
|
||||
<span className="text-sm text-neutral-400">{t('tailscale.urlPeriod')}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span>{t('tailscale.installed')}</span>
|
||||
<div className="flex justify-center">
|
||||
<Button type="primary" loading={isStarting} onClick={run}>
|
||||
{t('tailscale.login')}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{state === 'failed' && (
|
||||
<div className="flex flex-col space-y-10">
|
||||
<div className="flex flex-col items-center justify-center font-bold text-neutral-400">
|
||||
<span>{t('tailscale.failed')}</span>
|
||||
<span>{t('tailscale.retry')}</span>
|
||||
</div>
|
||||
|
||||
<ul className="list-decimal">
|
||||
<li>
|
||||
{t('tailscale.download')}
|
||||
<a
|
||||
className="px-1"
|
||||
href="https://pkgs.tailscale.com/stable/tailscale_latest_riscv64.tgz"
|
||||
target="_blank"
|
||||
>
|
||||
{t('tailscale.package')}
|
||||
</a>
|
||||
{t('tailscale.unzip')}
|
||||
</li>
|
||||
<li>{t('tailscale.upTailscale')}</li>
|
||||
<li>{t('tailscale.upTailscaled')}</li>
|
||||
<li>{t('tailscale.refresh')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
121
web/src/pages/desktop/menu-phone/settings/tailscale/device.tsx
Normal file
121
web/src/pages/desktop/menu-phone/settings/tailscale/device.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { LogoutOutlined } from '@ant-design/icons';
|
||||
import { Button, Divider, Switch } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { logoutTailscale, updateTailscaleStatus } from '@/api/network.ts';
|
||||
|
||||
type DeviceProps = {
|
||||
status: string;
|
||||
name: string;
|
||||
ip: string;
|
||||
account: string;
|
||||
setStatus: (status: 'notLogin') => void;
|
||||
};
|
||||
|
||||
export const Device = ({ status, name, ip, account, setStatus }: DeviceProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [isRunning, setIsRunning] = useState(false);
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
const [showLogoutConfirmation, setShowLogoutConfirmation] = useState(false);
|
||||
const [isLoggingOut, setIsLoggingOut] = useState(false);
|
||||
const [errMsg, setErrMsg] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setIsRunning(status === 'running');
|
||||
}, []);
|
||||
|
||||
function updateStatus() {
|
||||
if (isUpdating) return;
|
||||
setIsUpdating(true);
|
||||
|
||||
const command = isRunning ? 'down' : 'up';
|
||||
|
||||
updateTailscaleStatus(command)
|
||||
.then((rsp) => {
|
||||
if (rsp.code !== 0) {
|
||||
setErrMsg(rsp.msg);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsRunning(rsp.data.status === 'running');
|
||||
})
|
||||
.finally(() => {
|
||||
setIsUpdating(false);
|
||||
});
|
||||
}
|
||||
|
||||
function logout() {
|
||||
if (isLoggingOut) return;
|
||||
setIsLoggingOut(true);
|
||||
|
||||
logoutTailscale()
|
||||
.then((rsp) => {
|
||||
if (rsp.code !== 0) {
|
||||
setErrMsg(rsp.msg);
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus('notLogin');
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoggingOut(false);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full pt-10">
|
||||
<div className="flex justify-between px-3">
|
||||
<span>{t('tailscale.enable')}</span>
|
||||
<Switch checked={isRunning} loading={isUpdating} onClick={updateStatus} />
|
||||
</div>
|
||||
<Divider style={{ margin: '20px 0' }} />
|
||||
|
||||
<div className="flex justify-between px-3">
|
||||
<span>{t('tailscale.deviceName')}</span>
|
||||
<span>{name}</span>
|
||||
</div>
|
||||
<Divider style={{ margin: '20px 0' }} />
|
||||
|
||||
<div className="flex justify-between px-3">
|
||||
<span>{t('tailscale.deviceIP')}</span>
|
||||
<span>{ip}</span>
|
||||
</div>
|
||||
<Divider style={{ margin: '20px 0' }} />
|
||||
|
||||
<div className="flex justify-between px-3">
|
||||
<span>{t('tailscale.account')}</span>
|
||||
<span>{account}</span>
|
||||
</div>
|
||||
<Divider style={{ margin: '20px 0' }} />
|
||||
|
||||
<div className="flex justify-center py-5">
|
||||
{!showLogoutConfirmation ? (
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
shape="round"
|
||||
icon={<LogoutOutlined />}
|
||||
onClick={() => setShowLogoutConfirmation(true)}
|
||||
>
|
||||
{t('tailscale.logout')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
danger
|
||||
type="primary"
|
||||
size="large"
|
||||
icon={<LogoutOutlined />}
|
||||
loading={isLoggingOut}
|
||||
onClick={logout}
|
||||
>
|
||||
{t('tailscale.logout2')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{errMsg && <span className="text-red-500">{errMsg}</span>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,93 @@
|
||||
import { useState } from 'react';
|
||||
import { Modal } from 'antd';
|
||||
import { useSetAtom } from 'jotai/index';
|
||||
import { LoaderCircleIcon } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { getTailscaleStatus } from '@/api/network.ts';
|
||||
import { isSettingsOpenAtom } from '@/jotai/settings.ts';
|
||||
|
||||
import { Device } from './device.tsx';
|
||||
import { Install } from './install.tsx';
|
||||
import { Login } from './login.tsx';
|
||||
|
||||
type Status = 'notInstall' | 'notLogin' | 'stopped' | 'running';
|
||||
|
||||
export const Tailscale = () => {
|
||||
const { t } = useTranslation();
|
||||
const setIsSettingsOpen = useSetAtom(isSettingsOpenAtom);
|
||||
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [status, setStatus] = useState<Status>('notInstall');
|
||||
const [name, setName] = useState('');
|
||||
const [ip, setIp] = useState('');
|
||||
const [account, setAccount] = useState('');
|
||||
const [errMsg, setErrMsg] = useState('');
|
||||
|
||||
function openModal() {
|
||||
getStatus();
|
||||
|
||||
setIsModalOpen(true);
|
||||
setIsSettingsOpen(false);
|
||||
}
|
||||
|
||||
function getStatus() {
|
||||
if (isLoading) return;
|
||||
setIsLoading(true);
|
||||
|
||||
getTailscaleStatus()
|
||||
.then((rsp) => {
|
||||
if (rsp.code !== 0) {
|
||||
setErrMsg(rsp.msg);
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus(rsp.data.status);
|
||||
setName(rsp.data.name);
|
||||
setIp(rsp.data.ip);
|
||||
setAccount(rsp.data.account);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="flex cursor-pointer select-none items-center rounded px-3 py-1.5 hover:bg-neutral-600"
|
||||
onClick={openModal}
|
||||
>
|
||||
Tailscale
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
title="Tailscale"
|
||||
open={isModalOpen}
|
||||
width={360}
|
||||
footer={null}
|
||||
centered={true}
|
||||
maskClosable={false}
|
||||
onCancel={() => setIsModalOpen(false)}
|
||||
>
|
||||
<div className="flex min-h-[200px] flex-col items-center justify-center space-y-3">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center space-x-2 text-neutral-500">
|
||||
<LoaderCircleIcon className="animate-spin" size={18} />
|
||||
<span>{t('tailscale.loading')}</span>
|
||||
</div>
|
||||
) : status === 'notInstall' ? (
|
||||
<Install onSuccess={getStatus} />
|
||||
) : status === 'notLogin' ? (
|
||||
<Login onSuccess={getStatus} />
|
||||
) : (
|
||||
<Device status={status} ip={ip} account={account} name={name} setStatus={setStatus} />
|
||||
)}
|
||||
|
||||
{errMsg && <span className="text-red-500">{errMsg}</span>}
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
import { useState } from 'react';
|
||||
import { DownloadOutlined } from '@ant-design/icons';
|
||||
import { Button, Card } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { installTailscale } from '@/api/network.ts';
|
||||
|
||||
type InstallProps = {
|
||||
onSuccess: () => void;
|
||||
};
|
||||
|
||||
export const Install = ({ onSuccess }: InstallProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [isInstalling, setIsInstalling] = useState(false);
|
||||
const [isFailed, setIsFailed] = useState(false);
|
||||
|
||||
function install() {
|
||||
if (isInstalling) return;
|
||||
setIsInstalling(true);
|
||||
|
||||
installTailscale()
|
||||
.then((rsp) => {
|
||||
if (rsp.code !== 0) {
|
||||
setIsFailed(true);
|
||||
return;
|
||||
}
|
||||
|
||||
onSuccess();
|
||||
})
|
||||
.finally(() => {
|
||||
setIsInstalling(false);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isFailed ? (
|
||||
<div className="flex h-full flex-col items-center justify-center space-y-7">
|
||||
<span>{t('tailscale.notInstall')}</span>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
shape="round"
|
||||
icon={<DownloadOutlined />}
|
||||
loading={isInstalling}
|
||||
onClick={install}
|
||||
>
|
||||
{isInstalling ? t('tailscale.installing') : t('tailscale.install')}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col space-y-5 py-5">
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<span className="text-lg font-bold">{t('tailscale.failed')}</span>
|
||||
<span className="text-neutral-400">{t('tailscale.retry')}</span>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<ul className="list-decimal font-mono text-sm">
|
||||
<li>
|
||||
{t('tailscale.download')}
|
||||
<a
|
||||
className="px-1"
|
||||
href="https://pkgs.tailscale.com/stable/tailscale_latest_riscv64.tgz"
|
||||
target="_blank"
|
||||
>
|
||||
{t('tailscale.package')}
|
||||
</a>
|
||||
{t('tailscale.unzip')}
|
||||
</li>
|
||||
<li>{t('tailscale.upTailscale')}</li>
|
||||
<li>{t('tailscale.upTailscaled')}</li>
|
||||
<li>{t('tailscale.refresh')}</li>
|
||||
</ul>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
import { useState } from 'react';
|
||||
import { UserSwitchOutlined } from '@ant-design/icons';
|
||||
import { Button, Card } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { loginTailscale } from '@/api/network.ts';
|
||||
|
||||
type LoginProps = {
|
||||
onSuccess: () => void;
|
||||
};
|
||||
|
||||
export const Login = ({ onSuccess }: LoginProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [isLogging, setIsLogging] = useState(false);
|
||||
const [loginUrl, setLoginUrl] = useState('');
|
||||
const [errMsg, setErrMsg] = useState('');
|
||||
|
||||
function login() {
|
||||
if (isLogging) return;
|
||||
setIsLogging(true);
|
||||
|
||||
loginTailscale()
|
||||
.then((rsp) => {
|
||||
if (rsp.code !== 0) {
|
||||
setErrMsg(rsp.msg);
|
||||
return;
|
||||
}
|
||||
|
||||
// already logged in
|
||||
if (rsp.data.status === 'running') {
|
||||
onSuccess();
|
||||
return;
|
||||
}
|
||||
|
||||
const url = rsp.data.url;
|
||||
if (!url) return;
|
||||
|
||||
setLoginUrl(url);
|
||||
window.open(url, '_blank');
|
||||
setTimeout(() => setLoginUrl(''), 10 * 60 * 1000);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLogging(false);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center space-y-2">
|
||||
<Card style={{ margin: '20px 0 20px 0' }}>{t('tailscale.notLogin')}</Card>
|
||||
|
||||
{!loginUrl ? (
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
shape="round"
|
||||
icon={<UserSwitchOutlined />}
|
||||
loading={isLogging}
|
||||
onClick={login}
|
||||
>
|
||||
{t('tailscale.login')}
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
<Button type="link" href={loginUrl} target="_blank">
|
||||
{loginUrl}
|
||||
</Button>
|
||||
|
||||
<span className="text-sm text-neutral-500">{t('tailscale.urlPeriod')}</span>
|
||||
|
||||
<Button type="primary" size="large" shape="round" onClick={onSuccess}>
|
||||
{t('tailscale.loginSuccess')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{errMsg && <span className="text-red-500">{errMsg}</span>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,14 +1,14 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { CheckIcon } from 'lucide-react';
|
||||
import { Switch } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import * as api from '@/api/virtual-device';
|
||||
|
||||
export const VirtualDevice = () => {
|
||||
export const VirtualDevices = () => {
|
||||
const { t } = useTranslation();
|
||||
const [isNetworkOn, setIsNetworkOn] = useState(false);
|
||||
const [isUSBOn, setIsUSBOn] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [loading, setLoading] = useState(''); // '' | 'network' | 'usb'
|
||||
|
||||
useEffect(() => {
|
||||
api.getVirtualDevice().then((rsp) => {
|
||||
@@ -23,8 +23,8 @@ export const VirtualDevice = () => {
|
||||
}, []);
|
||||
|
||||
function update(device: string) {
|
||||
if (isLoading) return;
|
||||
setIsLoading(true);
|
||||
if (loading) return;
|
||||
setLoading(device);
|
||||
|
||||
api
|
||||
.updateVirtualDevice(device)
|
||||
@@ -41,25 +41,26 @@ export const VirtualDevice = () => {
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
setLoading('');
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="flex cursor-pointer select-none items-center space-x-1 rounded px-3 py-1.5 text-white hover:bg-neutral-600"
|
||||
className="flex cursor-pointer select-none items-center justify-between space-x-3 rounded px-3 py-1.5 text-white hover:bg-neutral-600"
|
||||
onClick={() => update('network')}
|
||||
>
|
||||
{isNetworkOn && <CheckIcon size={18} color="#3b82f6" />}
|
||||
<span>{t('virtualDevice.network')}</span>
|
||||
<div>{t('virtualDevice.network')}</div>
|
||||
<Switch size="small" checked={isNetworkOn} loading={loading === 'network'} />
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="flex cursor-pointer select-none items-center space-x-1 rounded px-3 py-1.5 text-white hover:bg-neutral-600"
|
||||
className="flex cursor-pointer select-none items-center justify-between space-x-3 rounded px-3 py-1.5 text-white hover:bg-neutral-600"
|
||||
onClick={() => update('usb')}
|
||||
>
|
||||
{isUSBOn && <CheckIcon size={18} color="#3b82f6" />}
|
||||
<span>{t('virtualDevice.usb')}</span>
|
||||
<div>{t('virtualDevice.usb')}</div>
|
||||
<Switch size="small" checked={isUSBOn} loading={loading === 'usb'} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
@@ -3,16 +3,16 @@ import { Divider } from 'antd';
|
||||
import clsx from 'clsx';
|
||||
import { MenuIcon, XIcon } from 'lucide-react';
|
||||
|
||||
import { Fullscreen } from './fullscreen';
|
||||
import { Fullscreen } from './fullscreen.tsx';
|
||||
import { Image } from './image';
|
||||
import { Keyboard } from './keyboard';
|
||||
import { Mouse } from './mouse';
|
||||
import { Power } from './power';
|
||||
import { Keyboard } from './keyboard.tsx';
|
||||
import { Mouse } from './mouse.tsx';
|
||||
import { Power } from './power.tsx';
|
||||
import { Screen } from './screen';
|
||||
import { Script } from './script';
|
||||
import { Settings } from './settings';
|
||||
import { Terminal } from './terminal';
|
||||
import { Wol } from './wol';
|
||||
import { Wol } from './wol.tsx';
|
||||
|
||||
export const Menu = () => {
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(true);
|
||||
|
||||
@@ -6,12 +6,12 @@ import { SettingsIcon } from 'lucide-react';
|
||||
import { isSettingsOpenAtom } from '@/jotai/settings.ts';
|
||||
|
||||
import { About } from './about.tsx';
|
||||
import { ChangePassword } from './change-password.tsx';
|
||||
import { Language } from './language.tsx';
|
||||
import { Logout } from './logout.tsx';
|
||||
import { Tailscale } from './tailscale.tsx';
|
||||
import { Password } from './password.tsx';
|
||||
import { Tailscale } from './tailscale';
|
||||
import { Update } from './update.tsx';
|
||||
import { VirtualDevice } from './virtual-device.tsx';
|
||||
import { VirtualDevices } from './virtual-devices.tsx';
|
||||
|
||||
export const Settings = () => {
|
||||
const [isSettingsOpen, setIsSettingsOpen] = useAtom(isSettingsOpenAtom);
|
||||
@@ -22,13 +22,15 @@ export const Settings = () => {
|
||||
<Language />
|
||||
<About />
|
||||
<Update setIsBadgeVisible={setIsBadgeVisible} />
|
||||
<Divider style={{ margin: '10px 0' }} />
|
||||
<Divider style={{ margin: '5px 0' }} />
|
||||
|
||||
<VirtualDevices />
|
||||
<Divider style={{ margin: '5px 0' }} />
|
||||
|
||||
<VirtualDevice />
|
||||
<Tailscale />
|
||||
<Divider style={{ margin: '10px 0' }} />
|
||||
<Divider style={{ margin: '5px 0' }} />
|
||||
|
||||
<ChangePassword />
|
||||
<Password />
|
||||
<Logout />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -34,7 +34,7 @@ export const Language = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover content={content} placement="right" trigger="hover" arrow>
|
||||
<Popover content={content} placement="rightTop" trigger="hover" arrow>
|
||||
<div className="flex cursor-pointer select-none items-center space-x-2 rounded px-3 py-1.5 text-white hover:bg-neutral-600">
|
||||
<LanguagesIcon size={16} />
|
||||
<span>{t('language')}</span>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
export const ChangePassword = () => {
|
||||
export const Password = () => {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -1,202 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { Button, Modal } from 'antd';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { LoaderCircleIcon } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import * as api from '@/api/network';
|
||||
import { isSettingsOpenAtom } from '@/jotai/settings.ts';
|
||||
|
||||
type State = '' | 'installing' | 'installed' | 'failed';
|
||||
|
||||
export const Tailscale = () => {
|
||||
const { t } = useTranslation();
|
||||
const setIsSettingsOpen = useSetAtom(isSettingsOpenAtom);
|
||||
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isStarting, setIsStarting] = useState(false);
|
||||
const [state, setState] = useState<State>('');
|
||||
const [loginUrl, setLoginUrl] = useState('');
|
||||
|
||||
function openModal() {
|
||||
check();
|
||||
|
||||
setIsModalOpen(true);
|
||||
setIsSettingsOpen(false);
|
||||
}
|
||||
|
||||
function check() {
|
||||
if (['installing', 'installed'].includes(state)) return;
|
||||
|
||||
if (isLoading) return;
|
||||
setIsLoading(true);
|
||||
|
||||
api
|
||||
.getTailscale()
|
||||
.then((rsp) => {
|
||||
if (rsp.code !== 0) {
|
||||
console.log(rsp.msg);
|
||||
return;
|
||||
}
|
||||
|
||||
if (rsp.data.exist) {
|
||||
setState('installed');
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
function install() {
|
||||
if (state === 'installing') return;
|
||||
setState('installing');
|
||||
|
||||
api
|
||||
.installTailscale()
|
||||
.then((rsp) => {
|
||||
if (rsp.code !== 0) {
|
||||
console.log(rsp.msg);
|
||||
setState('failed');
|
||||
return;
|
||||
}
|
||||
|
||||
setState('installed');
|
||||
})
|
||||
.catch(() => {
|
||||
setState('failed');
|
||||
});
|
||||
}
|
||||
|
||||
function run() {
|
||||
if (state !== 'installed') return;
|
||||
|
||||
if (isStarting) return;
|
||||
setIsStarting(true);
|
||||
|
||||
setLoginUrl('');
|
||||
|
||||
api
|
||||
.runTailscale()
|
||||
.then((rsp) => {
|
||||
if (rsp.code !== 0) {
|
||||
console.log(rsp.msg);
|
||||
return;
|
||||
}
|
||||
|
||||
if (rsp.data.url) {
|
||||
updateLoginUrl(rsp.data.url);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setIsStarting(false);
|
||||
});
|
||||
}
|
||||
|
||||
function updateLoginUrl(url: string) {
|
||||
setLoginUrl(url);
|
||||
|
||||
window.open(url, '_blank');
|
||||
|
||||
setTimeout(() => setLoginUrl(''), 10 * 60 * 1000);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="flex cursor-pointer select-none items-center rounded px-3 py-1.5 hover:bg-neutral-600"
|
||||
onClick={openModal}
|
||||
>
|
||||
Tailscale
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
title="Tailscale"
|
||||
open={isModalOpen}
|
||||
width={450}
|
||||
footer={null}
|
||||
centered={true}
|
||||
maskClosable={false}
|
||||
onCancel={() => setIsModalOpen(false)}
|
||||
>
|
||||
<div className="flex h-[200px] items-center justify-center">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center space-x-2 text-neutral-500">
|
||||
<LoaderCircleIcon className="animate-spin" size={18} />
|
||||
<span>{t('tailscale.loading')}</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{state === '' && (
|
||||
<div className="flex h-full flex-col items-center justify-center space-y-5">
|
||||
<span>{t('tailscale.notInstalled')}</span>
|
||||
<div className="flex justify-center">
|
||||
<Button type="primary" onClick={install}>
|
||||
{t('tailscale.install')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{state === 'installing' && (
|
||||
<div className="flex items-center justify-center space-x-2 text-neutral-500">
|
||||
<LoaderCircleIcon className="animate-spin" size={18} />
|
||||
<span>{t('tailscale.installing')}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{state === 'installed' && (
|
||||
<div className="flex h-full flex-col items-center justify-center space-y-5">
|
||||
{loginUrl ? (
|
||||
<>
|
||||
<a href={loginUrl} target="_blank">
|
||||
{loginUrl}
|
||||
</a>
|
||||
<span className="text-sm text-neutral-400">{t('tailscale.urlPeriod')}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span>{t('tailscale.installed')}</span>
|
||||
<div className="flex justify-center">
|
||||
<Button type="primary" loading={isStarting} onClick={run}>
|
||||
{t('tailscale.login')}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{state === 'failed' && (
|
||||
<div className="flex flex-col space-y-10">
|
||||
<div className="flex flex-col items-center justify-center font-bold text-neutral-400">
|
||||
<span>{t('tailscale.failed')}</span>
|
||||
<span>{t('tailscale.retry')}</span>
|
||||
</div>
|
||||
|
||||
<ul className="list-decimal">
|
||||
<li>
|
||||
{t('tailscale.download')}
|
||||
<a
|
||||
className="px-1"
|
||||
href="https://pkgs.tailscale.com/stable/tailscale_latest_riscv64.tgz"
|
||||
target="_blank"
|
||||
>
|
||||
{t('tailscale.package')}
|
||||
</a>
|
||||
{t('tailscale.unzip')}
|
||||
</li>
|
||||
<li>{t('tailscale.upTailscale')}</li>
|
||||
<li>{t('tailscale.upTailscaled')}</li>
|
||||
<li>{t('tailscale.refresh')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
121
web/src/pages/desktop/menu/settings/tailscale/device.tsx
Normal file
121
web/src/pages/desktop/menu/settings/tailscale/device.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { LogoutOutlined } from '@ant-design/icons';
|
||||
import { Button, Divider, Switch } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { logoutTailscale, updateTailscaleStatus } from '@/api/network.ts';
|
||||
|
||||
type DeviceProps = {
|
||||
status: string;
|
||||
name: string;
|
||||
ip: string;
|
||||
account: string;
|
||||
setStatus: (status: 'notLogin') => void;
|
||||
};
|
||||
|
||||
export const Device = ({ status, name, ip, account, setStatus }: DeviceProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [isRunning, setIsRunning] = useState(false);
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
const [showLogoutConfirmation, setShowLogoutConfirmation] = useState(false);
|
||||
const [isLoggingOut, setIsLoggingOut] = useState(false);
|
||||
const [errMsg, setErrMsg] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setIsRunning(status === 'running');
|
||||
}, []);
|
||||
|
||||
function updateStatus() {
|
||||
if (isUpdating) return;
|
||||
setIsUpdating(true);
|
||||
|
||||
const command = isRunning ? 'down' : 'up';
|
||||
|
||||
updateTailscaleStatus(command)
|
||||
.then((rsp) => {
|
||||
if (rsp.code !== 0) {
|
||||
setErrMsg(rsp.msg);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsRunning(rsp.data.status === 'running');
|
||||
})
|
||||
.finally(() => {
|
||||
setIsUpdating(false);
|
||||
});
|
||||
}
|
||||
|
||||
function logout() {
|
||||
if (isLoggingOut) return;
|
||||
setIsLoggingOut(true);
|
||||
|
||||
logoutTailscale()
|
||||
.then((rsp) => {
|
||||
if (rsp.code !== 0) {
|
||||
setErrMsg(rsp.msg);
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus('notLogin');
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoggingOut(false);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full pt-10">
|
||||
<div className="flex justify-between px-3">
|
||||
<span>{t('tailscale.enable')}</span>
|
||||
<Switch checked={isRunning} loading={isUpdating} onClick={updateStatus} />
|
||||
</div>
|
||||
<Divider style={{ margin: '20px 0' }} />
|
||||
|
||||
<div className="flex justify-between px-3">
|
||||
<span>{t('tailscale.deviceName')}</span>
|
||||
<span>{name}</span>
|
||||
</div>
|
||||
<Divider style={{ margin: '20px 0' }} />
|
||||
|
||||
<div className="flex justify-between px-3">
|
||||
<span>{t('tailscale.deviceIP')}</span>
|
||||
<span>{ip}</span>
|
||||
</div>
|
||||
<Divider style={{ margin: '20px 0' }} />
|
||||
|
||||
<div className="flex justify-between px-3">
|
||||
<span>{t('tailscale.account')}</span>
|
||||
<span>{account}</span>
|
||||
</div>
|
||||
<Divider style={{ margin: '20px 0' }} />
|
||||
|
||||
<div className="flex justify-center py-5">
|
||||
{!showLogoutConfirmation ? (
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
shape="round"
|
||||
icon={<LogoutOutlined />}
|
||||
onClick={() => setShowLogoutConfirmation(true)}
|
||||
>
|
||||
{t('tailscale.logout')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
danger
|
||||
type="primary"
|
||||
size="large"
|
||||
icon={<LogoutOutlined />}
|
||||
loading={isLoggingOut}
|
||||
onClick={logout}
|
||||
>
|
||||
{t('tailscale.logout2')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{errMsg && <span className="text-red-500">{errMsg}</span>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
93
web/src/pages/desktop/menu/settings/tailscale/index.tsx
Normal file
93
web/src/pages/desktop/menu/settings/tailscale/index.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import { useState } from 'react';
|
||||
import { Modal } from 'antd';
|
||||
import { useSetAtom } from 'jotai/index';
|
||||
import { LoaderCircleIcon } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { getTailscaleStatus } from '@/api/network.ts';
|
||||
import { isSettingsOpenAtom } from '@/jotai/settings.ts';
|
||||
|
||||
import { Device } from './device.tsx';
|
||||
import { Install } from './install.tsx';
|
||||
import { Login } from './login.tsx';
|
||||
|
||||
type Status = 'notInstall' | 'notLogin' | 'stopped' | 'running';
|
||||
|
||||
export const Tailscale = () => {
|
||||
const { t } = useTranslation();
|
||||
const setIsSettingsOpen = useSetAtom(isSettingsOpenAtom);
|
||||
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [status, setStatus] = useState<Status>('notInstall');
|
||||
const [name, setName] = useState('');
|
||||
const [ip, setIp] = useState('');
|
||||
const [account, setAccount] = useState('');
|
||||
const [errMsg, setErrMsg] = useState('');
|
||||
|
||||
function openModal() {
|
||||
getStatus();
|
||||
|
||||
setIsModalOpen(true);
|
||||
setIsSettingsOpen(false);
|
||||
}
|
||||
|
||||
function getStatus() {
|
||||
if (isLoading) return;
|
||||
setIsLoading(true);
|
||||
|
||||
getTailscaleStatus()
|
||||
.then((rsp) => {
|
||||
if (rsp.code !== 0) {
|
||||
setErrMsg(rsp.msg);
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus(rsp.data.status);
|
||||
setName(rsp.data.name);
|
||||
setIp(rsp.data.ip);
|
||||
setAccount(rsp.data.account);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="flex cursor-pointer select-none items-center rounded px-3 py-1.5 hover:bg-neutral-600"
|
||||
onClick={openModal}
|
||||
>
|
||||
Tailscale
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
title="Tailscale"
|
||||
open={isModalOpen}
|
||||
width={500}
|
||||
footer={null}
|
||||
centered={true}
|
||||
maskClosable={false}
|
||||
onCancel={() => setIsModalOpen(false)}
|
||||
>
|
||||
<div className="flex min-h-[200px] flex-col items-center justify-center space-y-3">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center space-x-2 text-neutral-500">
|
||||
<LoaderCircleIcon className="animate-spin" size={18} />
|
||||
<span>{t('tailscale.loading')}</span>
|
||||
</div>
|
||||
) : status === 'notInstall' ? (
|
||||
<Install onSuccess={getStatus} />
|
||||
) : status === 'notLogin' ? (
|
||||
<Login onSuccess={getStatus} />
|
||||
) : (
|
||||
<Device status={status} ip={ip} account={account} name={name} setStatus={setStatus} />
|
||||
)}
|
||||
|
||||
{errMsg && <span className="text-red-500">{errMsg}</span>}
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
82
web/src/pages/desktop/menu/settings/tailscale/install.tsx
Normal file
82
web/src/pages/desktop/menu/settings/tailscale/install.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import { useState } from 'react';
|
||||
import { DownloadOutlined } from '@ant-design/icons';
|
||||
import { Button, Card } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { installTailscale } from '@/api/network.ts';
|
||||
|
||||
type InstallProps = {
|
||||
onSuccess: () => void;
|
||||
};
|
||||
|
||||
export const Install = ({ onSuccess }: InstallProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [isInstalling, setIsInstalling] = useState(false);
|
||||
const [isFailed, setIsFailed] = useState(false);
|
||||
|
||||
function install() {
|
||||
if (isInstalling) return;
|
||||
setIsInstalling(true);
|
||||
|
||||
installTailscale()
|
||||
.then((rsp) => {
|
||||
if (rsp.code !== 0) {
|
||||
setIsFailed(true);
|
||||
return;
|
||||
}
|
||||
|
||||
onSuccess();
|
||||
})
|
||||
.finally(() => {
|
||||
setIsInstalling(false);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isFailed ? (
|
||||
<div className="flex h-full flex-col items-center justify-center space-y-7">
|
||||
<span>{t('tailscale.notInstall')}</span>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
shape="round"
|
||||
icon={<DownloadOutlined />}
|
||||
loading={isInstalling}
|
||||
onClick={install}
|
||||
>
|
||||
{isInstalling ? t('tailscale.installing') : t('tailscale.install')}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col space-y-5 py-5">
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<span className="text-lg font-bold">{t('tailscale.failed')}</span>
|
||||
<span className="text-neutral-400">{t('tailscale.retry')}</span>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<ul className="list-decimal font-mono text-sm">
|
||||
<li>
|
||||
{t('tailscale.download')}
|
||||
<a
|
||||
className="px-1"
|
||||
href="https://pkgs.tailscale.com/stable/tailscale_latest_riscv64.tgz"
|
||||
target="_blank"
|
||||
>
|
||||
{t('tailscale.package')}
|
||||
</a>
|
||||
{t('tailscale.unzip')}
|
||||
</li>
|
||||
<li>{t('tailscale.upTailscale')}</li>
|
||||
<li>{t('tailscale.upTailscaled')}</li>
|
||||
<li>{t('tailscale.refresh')}</li>
|
||||
</ul>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
80
web/src/pages/desktop/menu/settings/tailscale/login.tsx
Normal file
80
web/src/pages/desktop/menu/settings/tailscale/login.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import { useState } from 'react';
|
||||
import { UserSwitchOutlined } from '@ant-design/icons';
|
||||
import { Button, Card } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { loginTailscale } from '@/api/network.ts';
|
||||
|
||||
type LoginProps = {
|
||||
onSuccess: () => void;
|
||||
};
|
||||
|
||||
export const Login = ({ onSuccess }: LoginProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [isLogging, setIsLogging] = useState(false);
|
||||
const [loginUrl, setLoginUrl] = useState('');
|
||||
const [errMsg, setErrMsg] = useState('');
|
||||
|
||||
function login() {
|
||||
if (isLogging) return;
|
||||
setIsLogging(true);
|
||||
|
||||
loginTailscale()
|
||||
.then((rsp) => {
|
||||
if (rsp.code !== 0) {
|
||||
setErrMsg(rsp.msg);
|
||||
return;
|
||||
}
|
||||
|
||||
// already logged in
|
||||
if (rsp.data.status === 'running') {
|
||||
onSuccess();
|
||||
return;
|
||||
}
|
||||
|
||||
const url = rsp.data.url;
|
||||
if (!url) return;
|
||||
|
||||
setLoginUrl(url);
|
||||
window.open(url, '_blank');
|
||||
setTimeout(() => setLoginUrl(''), 10 * 60 * 1000);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLogging(false);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center space-y-2">
|
||||
<Card style={{ margin: '20px 0 20px 0' }}>{t('tailscale.notLogin')}</Card>
|
||||
|
||||
{!loginUrl ? (
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
shape="round"
|
||||
icon={<UserSwitchOutlined />}
|
||||
loading={isLogging}
|
||||
onClick={login}
|
||||
>
|
||||
{t('tailscale.login')}
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
<Button type="link" href={loginUrl} target="_blank">
|
||||
{loginUrl}
|
||||
</Button>
|
||||
|
||||
<span className="text-sm text-neutral-500">{t('tailscale.urlPeriod')}</span>
|
||||
|
||||
<Button type="primary" size="large" shape="round" onClick={onSuccess}>
|
||||
{t('tailscale.loginSuccess')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{errMsg && <span className="text-red-500">{errMsg}</span>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,14 +1,14 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { CheckIcon } from 'lucide-react';
|
||||
import { Switch } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import * as api from '@/api/virtual-device';
|
||||
|
||||
export const VirtualDevice = () => {
|
||||
export const VirtualDevices = () => {
|
||||
const { t } = useTranslation();
|
||||
const [isNetworkOn, setIsNetworkOn] = useState(false);
|
||||
const [isUSBOn, setIsUSBOn] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [loading, setLoading] = useState(''); // '' | 'network' | 'usb'
|
||||
|
||||
useEffect(() => {
|
||||
api.getVirtualDevice().then((rsp) => {
|
||||
@@ -23,8 +23,8 @@ export const VirtualDevice = () => {
|
||||
}, []);
|
||||
|
||||
function update(device: string) {
|
||||
if (isLoading) return;
|
||||
setIsLoading(true);
|
||||
if (loading) return;
|
||||
setLoading(device);
|
||||
|
||||
api
|
||||
.updateVirtualDevice(device)
|
||||
@@ -41,25 +41,26 @@ export const VirtualDevice = () => {
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
setLoading('');
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="flex cursor-pointer select-none items-center space-x-1 rounded px-3 py-1.5 text-white hover:bg-neutral-600"
|
||||
className="flex cursor-pointer select-none items-center justify-between space-x-3 rounded px-3 py-1.5 text-white hover:bg-neutral-600"
|
||||
onClick={() => update('network')}
|
||||
>
|
||||
{isNetworkOn && <CheckIcon size={18} color="#3b82f6" />}
|
||||
<span>{t('virtualDevice.network')}</span>
|
||||
<div>{t('virtualDevice.network')}</div>
|
||||
<Switch size="small" checked={isNetworkOn} loading={loading === 'network'} />
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="flex cursor-pointer select-none items-center space-x-1 rounded px-3 py-1.5 text-white hover:bg-neutral-600"
|
||||
className="flex cursor-pointer select-none items-center justify-between space-x-3 rounded px-3 py-1.5 text-white hover:bg-neutral-600"
|
||||
onClick={() => update('usb')}
|
||||
>
|
||||
{isUSBOn && <CheckIcon size={18} color="#3b82f6" />}
|
||||
<span>{t('virtualDevice.usb')}</span>
|
||||
<div>{t('virtualDevice.usb')}</div>
|
||||
<Switch size="small" checked={isUSBOn} loading={loading === 'usb'} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
@@ -1,4 +1,4 @@
|
||||
import { TerminalIcon } from 'lucide-react';
|
||||
import { SquareTerminalIcon } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
type NanokvmProps = {
|
||||
@@ -15,10 +15,10 @@ export const Nanokvm = ({ setIsPopoverOpen }: NanokvmProps) => {
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex h-[28px] cursor-pointer select-none items-center space-x-2 rounded px-2 py-1 hover:bg-neutral-700/80"
|
||||
className="flex h-[28px] cursor-pointer select-none items-center space-x-1 rounded px-2 py-1 hover:bg-neutral-700/80"
|
||||
onClick={openTerminal}
|
||||
>
|
||||
<TerminalIcon size={16} />
|
||||
<SquareTerminalIcon size={14} />
|
||||
<span>{t('terminal.nanokvm')}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ChangeEvent, useState } from 'react';
|
||||
import { Button, Input, InputNumber, Modal, Radio, RadioChangeEvent } from 'antd';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { TerminalIcon } from 'lucide-react';
|
||||
import { SquareTerminalIcon } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { isKeyboardEnableAtom } from '@/jotai/keyboard.ts';
|
||||
@@ -56,10 +56,10 @@ export const SerialPort = ({ setIsPopoverOpen }: SerialPortProps) => {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="flex h-[28px] cursor-pointer select-none items-center space-x-2 rounded px-2 py-1 hover:bg-neutral-700/80"
|
||||
className="flex h-[28px] cursor-pointer select-none items-center space-x-1 rounded px-2 py-1 hover:bg-neutral-700/80"
|
||||
onClick={openModal}
|
||||
>
|
||||
<TerminalIcon size={16} />
|
||||
<SquareTerminalIcon size={14} />
|
||||
<span>{t('terminal.serial')}</span>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user