login i18n & chart loading & fix forgot pass page (#236)

Co-authored-by: hank <hank@henrygd.me>
This commit is contained in:
ArsFy
2024-10-30 11:22:03 +08:00
committed by GitHub
parent 062796b38c
commit 180ec83a17
16 changed files with 369 additions and 205 deletions

Binary file not shown.

View File

@@ -32,7 +32,6 @@
"cmdk": "^1.0.0",
"d3-time": "^3.1.0",
"i18next": "^23.16.4",
"i18next-browser-languagedetector": "^8.0.0",
"lucide-react": "^0.452.0",
"nanostores": "^0.11.3",
"pocketbase": "^0.21.5",

View File

@@ -16,6 +16,7 @@ import {
import { useCallback, useState } from 'react'
import { AuthMethodsList, OAuth2AuthConfig } from 'pocketbase'
import { Link } from '../router'
import { useTranslation } from 'react-i18next'
const honeypot = v.literal('')
const emailSchema = v.pipe(v.string(), v.email('Invalid email address.'))
@@ -63,6 +64,8 @@ export function UserAuthForm({
isFirstRun: boolean
authMethods: AuthMethodsList
}) {
const { t } = useTranslation()
const [isLoading, setIsLoading] = useState<boolean>(false)
const [isOauthLoading, setIsOauthLoading] = useState<boolean>(false)
const [errors, setErrors] = useState<Record<string, string | undefined>>({})
@@ -224,7 +227,7 @@ export function UserAuthForm({
) : (
<LogInIcon className="mr-2 h-4 w-4" />
)}
{isFirstRun ? 'Create account' : 'Sign in'}
{isFirstRun ? t('auth.create_account') : t('auth.sign_in')}
</button>
</div>
</form>
@@ -317,16 +320,16 @@ export function UserAuthForm({
<DialogTitle>OAuth 2 / OIDC support</DialogTitle>
</DialogHeader>
<div className="text-primary/70 text-[0.95em] contents">
<p>Beszel supports OpenID Connect and many OAuth2 authentication providers.</p>
<p>{t('auth.openid_des')}</p>
<p>
Please view the{' '}
{t('please_view_the')}{' '}
<a
href="https://github.com/henrygd/beszel/blob/main/readme.md#oauth--oidc-integration"
className={cn(buttonVariants({ variant: 'link' }), 'p-0 h-auto')}
>
GitHub README
</a>{' '}
for instructions.
{t('for_instructions')}
</p>
</div>
</DialogContent>
@@ -338,7 +341,7 @@ export function UserAuthForm({
href="/forgot-password"
className="text-sm mx-auto hover:text-brand underline underline-offset-4 opacity-70 hover:opacity-100 transition-opacity"
>
Forgot password?
{t('auth.forgot_password')}
</Link>
)}
</div>

View File

@@ -8,6 +8,7 @@ import { cn } from '@/lib/utils'
import { pb } from '@/lib/stores'
import { Dialog, DialogHeader } from '../ui/dialog'
import { DialogContent, DialogTrigger, DialogTitle } from '../ui/dialog'
import { useTranslation } from 'react-i18next'
const showLoginFaliedToast = () => {
toast({
@@ -18,6 +19,7 @@ const showLoginFaliedToast = () => {
}
export default function ForgotPassword() {
const { t } = useTranslation()
const [isLoading, setIsLoading] = useState<boolean>(false)
const [email, setEmail] = useState('')
@@ -72,26 +74,25 @@ export default function ForgotPassword() {
) : (
<SendHorizonalIcon className="mr-2 h-4 w-4" />
)}
Reset password
{t('auth.reset_password')}
</button>
</div>
</form>
<Dialog>
<DialogTrigger asChild>
<button className="text-sm mx-auto hover:text-brand underline underline-offset-4 opacity-70 hover:opacity-100 transition-opacity">
Command line instructions
{t('auth.command_line_instructions')}
</button>
</DialogTrigger>
<DialogContent className="max-w-[33em]">
<DialogHeader>
<DialogTitle>Command line instructions</DialogTitle>
<DialogTitle>{t('auth.command_line_instructions')}</DialogTitle>
</DialogHeader>
<p className="text-primary/70 text-[0.95em] leading-relaxed">
If you've lost the password to your admin account, you may reset it using the following
command.
{t('auth.command_1')}
</p>
<p className="text-primary/70 text-[0.95em] leading-relaxed">
Then log into the backend and reset your user account password in the users table.
{t('auth.command_2')}
</p>
<code className="bg-muted rounded-sm py-0.5 px-2.5 mr-auto text-sm">
beszel admin update youremail@example.com newpassword

View File

@@ -6,8 +6,11 @@ import { useStore } from '@nanostores/react'
import ForgotPassword from './forgot-pass-form'
import { $router } from '../router'
import { AuthMethodsList } from 'pocketbase'
import { useTranslation } from 'react-i18next'
export default function () {
const { t } = useTranslation()
const page = useStore($router)
const [isFirstRun, setFirstRun] = useState(false)
const [authMethods, setAuthMethods] = useState<AuthMethodsList>()
@@ -30,11 +33,11 @@ export default function () {
const subtitle = useMemo(() => {
if (isFirstRun) {
return 'Please create an admin account'
return t('auth.create')
} else if (page?.path === '/forgot-password') {
return 'Enter email address to reset password'
return t('auth.reset')
} else {
return 'Please sign in to your account'
return t('auth.login')
}
}, [isFirstRun, page])

View File

@@ -5,6 +5,7 @@ export const $router = createRouter(
home: '/',
server: '/system/:name',
settings: '/settings/:name?',
forgot_password: '/forgot-password',
},
{ links: false }
)

View File

@@ -112,6 +112,7 @@ export default function SystemDetail({ name }: { name: string }) {
const netCardRef = useRef<HTMLDivElement>(null)
const [containerFilterBar, setContainerFilterBar] = useState(null as null | JSX.Element)
const [bottomSpacing, setBottomSpacing] = useState(0)
const [chartLoading, setChartLoading] = useState(false)
const isLongerChart = chartTime !== '1h'
useEffect(() => {
@@ -178,10 +179,15 @@ export default function SystemDetail({ name }: { name: string }) {
if (!system.id || !chartTime) {
return
}
// loading: true
setChartLoading(true)
Promise.allSettled([
getStats<SystemStatsRecord>('system_stats', system, chartTime),
getStats<ContainerStatsRecord>('container_stats', system, chartTime),
]).then(([systemStats, containerStats]) => {
// loading: false
setChartLoading(false)
const { expectedInterval } = chartTimeData[chartTime]
// make new system stats
const ss_cache_key = `${system.id}_${chartTime}_system_stats`
@@ -291,6 +297,9 @@ export default function SystemDetail({ name }: { name: string }) {
return null
}
// if no data, show empty state
const dataEmpty = !chartLoading && chartData.systemStats.length === 0;
return (
<>
<div id="chartwrap" className="grid gap-4 mb-10 overflow-x-clip">
@@ -375,6 +384,7 @@ export default function SystemDetail({ name }: { name: string }) {
{/* main charts */}
<div className="grid lg:grid-cols-2 gap-4">
<ChartCard
empty={dataEmpty}
grid={grid}
title={t('monitor.total_cpu_usage')}
description={`${cpuMaxStore[0] && isLongerChart ? t('monitor.max_1_min') : t('monitor.average') } ${t('monitor.cpu_des')}`}
@@ -390,6 +400,7 @@ export default function SystemDetail({ name }: { name: string }) {
{containerFilterBar && (
<ChartCard
empty={dataEmpty}
grid={grid}
title={t('monitor.docker_cpu_usage')}
description={t('monitor.docker_cpu_des')}
@@ -400,6 +411,7 @@ export default function SystemDetail({ name }: { name: string }) {
)}
<ChartCard
empty={dataEmpty}
grid={grid}
title={t('monitor.total_memory_usage')}
description={t('monitor.memory_des')}
@@ -409,6 +421,7 @@ export default function SystemDetail({ name }: { name: string }) {
{containerFilterBar && (
<ChartCard
empty={dataEmpty}
grid={grid}
title={t('monitor.docker_memory_usage')}
description={t('monitor.docker_memory_des')}
@@ -418,7 +431,7 @@ export default function SystemDetail({ name }: { name: string }) {
</ChartCard>
)}
<ChartCard grid={grid} title={t('monitor.disk_space')} description={t('monitor.disk_des')}>
<ChartCard empty={dataEmpty} grid={grid} title={t('monitor.disk_space')} description={t('monitor.disk_des')}>
<DiskChart
chartData={chartData}
dataKey="stats.du"
@@ -427,6 +440,7 @@ export default function SystemDetail({ name }: { name: string }) {
</ChartCard>
<ChartCard
empty={dataEmpty}
grid={grid}
title={t('monitor.disk_io')}
description={t('monitor.disk_io_des')}
@@ -440,6 +454,7 @@ export default function SystemDetail({ name }: { name: string }) {
</ChartCard>
<ChartCard
empty={dataEmpty}
grid={grid}
title={t('monitor.bandwidth')}
cornerEl={isLongerChart ? <SelectAvgMax store={bandwidthMaxStore} /> : null}
@@ -460,6 +475,7 @@ export default function SystemDetail({ name }: { name: string }) {
})}
>
<ChartCard
empty={dataEmpty}
title={t('monitor.docker_network_io')}
description={t('monitor.docker_network_io_des')}
cornerEl={containerFilterBar}
@@ -471,13 +487,13 @@ export default function SystemDetail({ name }: { name: string }) {
)}
{(systemStats.at(-1)?.stats.su ?? 0) > 0 && (
<ChartCard grid={grid} title={t('monitor.swap_usage')} description={t('monitor.swap_des')}>
<ChartCard empty={dataEmpty} grid={grid} title={t('monitor.swap_usage')} description={t('monitor.swap_des')}>
<SwapChart chartData={chartData} />
</ChartCard>
)}
{systemStats.at(-1)?.stats.t && (
<ChartCard grid={grid} title={t('monitor.temperature')} description={t('monitor.temperature_des')}>
<ChartCard empty={dataEmpty} grid={grid} title={t('monitor.temperature')} description={t('monitor.temperature_des')}>
<TemperatureChart chartData={chartData} />
</ChartCard>
)}
@@ -490,6 +506,7 @@ export default function SystemDetail({ name }: { name: string }) {
return (
<div key={extraFsName} className="contents">
<ChartCard
empty={dataEmpty}
grid={grid}
title={`${extraFsName} ${t('monitor.usage')}`}
description={`${t('monitor.disk_usage_of')} ${extraFsName}`}
@@ -501,6 +518,7 @@ export default function SystemDetail({ name }: { name: string }) {
/>
</ChartCard>
<ChartCard
empty={dataEmpty}
grid={grid}
title={`${extraFsName} I/O`}
description={`${t('monitor.throughput_of')} ${extraFsName}`}
@@ -591,12 +609,14 @@ function ChartCard({
description,
children,
grid,
empty,
cornerEl,
}: {
title: string
description: string
children: React.ReactNode
grid?: boolean
empty?: boolean,
cornerEl?: JSX.Element | null
}) {
const { isIntersecting, ref } = useIntersectionObserver()
@@ -616,7 +636,7 @@ function ChartCard({
)}
</CardHeader>
<div className="pl-0 w-[calc(100%-1.6em)] h-52 relative">
{<Spinner />}
{<Spinner empty={empty} />}
{isIntersecting && children}
</div>
</Card>

View File

@@ -1,9 +1,12 @@
import { LoaderCircleIcon } from 'lucide-react'
import { useTranslation } from 'react-i18next'
export default function () {
export default function (props: { empty?: boolean }) {
const { t } = useTranslation()
return (
<div className="grid place-content-center h-full absolute inset-0">
<div className="flex flex-col items-center justify-center h-full absolute inset-0">
<LoaderCircleIcon className="animate-spin h-10 w-10 opacity-60" />
{props.empty && <p className={'opacity-60 mt-2'}>{t('monitor.waiting_for')}</p>}
</div>
)
}

View File

@@ -1,6 +1,5 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import en from '../locales/en/translation.json';
import es from '../locales/es/translation.json';
@@ -10,8 +9,32 @@ import ru from '../locales/ru/translation.json';
import zhHans from '../locales/zh-CN/translation.json';
import zhHant from '../locales/zh-HK/translation.json';
// Custom language detector to use localStorage
const languageDetector: any = {
type: 'languageDetector',
async: true,
detect: (callback: (lng: string) => void) => {
const savedLanguage = localStorage.getItem('i18nextLng');
const fallbackLanguage = (()=>{
switch (navigator.language) {
case 'zh-CN': case 'zh-SG': case 'zh-MY': case 'zh': case 'zh-Hans':
return 'zh-CN';
case 'zh-HK': case 'zh-TW': case 'zh-MO': case 'zh-Hant':
return 'zh-HK';
default:
return navigator.language;
}
})();
callback(savedLanguage || fallbackLanguage);
},
init: () => {},
cacheUserLanguage: (lng: string) => {
localStorage.setItem('i18nextLng', lng);
}
};
i18n
.use(LanguageDetector)
.use(languageDetector)
.use(initReactI18next)
.init({
resources: {

View File

@@ -173,6 +173,22 @@
"temperature_des": "Temperaturen der System-Sensoren",
"usage": "Nutzung",
"disk_usage_of": "Festplattennutzung von",
"throughput_of": "Durchsatz von"
"throughput_of": "Durchsatz von",
"waiting_for": "Warten auf genügend Datensätze zur Anzeige"
},
"auth": {
"login": "Bitte melden Sie sich bei Ihrem Konto an",
"reset": "E-Mail-Adresse eingeben, um das Passwort zurückzusetzen",
"create": "Bitte erstellen Sie ein Administratorkonto",
"create_account": "Konto erstellen",
"sign_in": "Anmelden",
"openid_des": "Beszel unterstützt OpenID Connect und viele OAuth2-Authentifizierungsanbieter.",
"please_view_the": "Bitte sehen Sie sich die",
"for_instructions": "für Anweisungen an.",
"forgot_password": "Passwort vergessen?",
"reset_password": "Passwort zurücksetzen",
"command_line_instructions": "Befehlszeilenanweisungen",
"command_1": "Wenn Sie das Passwort für Ihr Administratorkonto verloren haben, können Sie es mit dem folgenden Befehl zurücksetzen.",
"command_2": "Melden Sie sich dann im Backend an und setzen Sie das Passwort Ihres Benutzerkontos in der Benutzertabelle zurück."
}
}

View File

@@ -9,9 +9,8 @@
"continue": "Continue",
"home": {
"active_alerts": "Active Alerts",
"active_des": "Exceeds {{value}}{{unit}} average in last {{minutes}} min",
"active_des": "Exceeds {{value}}{{unit}} average in last {{minutes}} minutes",
"subtitle_1": "Updated in real time. Click on a system to view information.",
"subtitle_2": "to open the command palette."
},
"systems_table": {
"system": "System",
@@ -173,6 +172,22 @@
"temperature_des": "Temperatures of system sensors",
"usage": "Usage",
"disk_usage_of": "Disk usage of",
"throughput_of": "Throughput of"
"throughput_of": "Throughput of",
"waiting_for": "Waiting for enough records to display"
},
"auth": {
"login": "Please sign in to your account",
"reset": "Enter email address to reset password",
"create": "Please create an admin account",
"create_account": "Create account",
"sign_in": "Sign in",
"openid_des": "Beszel supports OpenID Connect and many OAuth2 authentication providers.",
"please_view_the": "Please view the",
"for_instructions": "for instructions.",
"forgot_password": "Forgot password?",
"reset_password": "Reset Password",
"command_line_instructions": "Command line instructions",
"command_1": "If you've lost the password to your admin account, you may reset it using the following command.",
"command_2": "Then log into the backend and reset your user account password in the users table."
}
}

View File

@@ -173,6 +173,22 @@
"temperature_des": "Temperaturas de los sensores del sistema",
"usage": "Uso",
"disk_usage_of": "Uso de disco de",
"throughput_of": "Rendimiento de"
"throughput_of": "Rendimiento de",
"waiting_for": "Esperando suficientes registros para mostrar"
},
"auth": {
"login": "Por favor, inicie sesión en su cuenta",
"reset": "Ingrese la dirección de correo electrónico para restablecer la contraseña",
"create": "Por favor, cree una cuenta de administrador",
"create_account": "Crear cuenta",
"sign_in": "Iniciar sesión",
"openid_des": "Beszel admite OpenID Connect y muchos proveedores de autenticación OAuth2.",
"please_view_the": "Por favor, consulte el",
"for_instructions": "para obtener instrucciones.",
"forgot_password": "¿Olvidó su contraseña?",
"reset_password": "Restablecer contraseña",
"command_line_instructions": "Instrucciones de línea de comandos",
"command_1": "Si ha perdido la contraseña de su cuenta de administrador, puede restablecerla usando el siguiente comando.",
"command_2": "Luego inicie sesión en el backend y restablezca la contraseña de su cuenta de usuario en la tabla de usuarios."
}
}

View File

@@ -173,6 +173,22 @@
"temperature_des": "Températures des capteurs du système",
"usage": "Utilisation",
"disk_usage_of": "Utilisation du disque de",
"throughput_of": "Débit de"
"throughput_of": "Débit de",
"waiting_for": "En attente de suffisamment d'enregistrements pour afficher"
},
"auth": {
"login": "Veuillez vous connecter à votre compte",
"reset": "Entrez l'adresse e-mail pour réinitialiser le mot de passe",
"create": "Veuillez créer un compte administrateur",
"create_account": "Créer un compte",
"sign_in": "Se connecter",
"openid_des": "Beszel prend en charge OpenID Connect et de nombreux fournisseurs d'authentification OAuth2.",
"please_view_the": "Veuillez consulter le",
"for_instructions": "pour les instructions.",
"forgot_password": "Mot de passe oublié ?",
"reset_password": "Réinitialiser le mot de passe",
"command_line_instructions": "Instructions en ligne de commande",
"command_1": "Si vous avez perdu le mot de passe de votre compte administrateur, vous pouvez le réinitialiser en utilisant la commande suivante.",
"command_2": "Ensuite, connectez-vous au backend et réinitialisez le mot de passe de votre compte utilisateur dans la table des utilisateurs."
}
}

View File

@@ -173,6 +173,22 @@
"temperature_des": "Температуры датчиков системы",
"usage": "Использование",
"disk_usage_of": "Использование диска",
"throughput_of": "Пропускная способность"
"throughput_of": "Пропускная способность",
"waiting_for": "Ожидание достаточного количества записей для отображения"
},
"auth": {
"login": "Пожалуйста, войдите в свою учетную запись",
"reset": "Введите адрес электронной почты для сброса пароля",
"create": "Пожалуйста, создайте учетную запись администратора",
"create_account": "Создать учетную запись",
"sign_in": "Войти",
"openid_des": "Beszel поддерживает OpenID Connect и многих поставщиков аутентификации OAuth2.",
"please_view_the": "Пожалуйста, ознакомьтесь с",
"for_instructions": "для получения инструкций.",
"forgot_password": "Забыли пароль?",
"reset_password": "Сбросить пароль",
"command_line_instructions": "Инструкции по командной строке",
"command_1": "Если вы потеряли пароль от своей учетной записи администратора, вы можете сбросить его, используя следующую команду.",
"command_2": "Затем войдите в бэкэнд и сбросьте пароль своей учетной записи пользователя в таблице пользователей."
}
}

View File

@@ -173,6 +173,22 @@
"temperature_des": "系统传感器的温度",
"usage": "使用率",
"disk_usage_of": "的磁盘使用率",
"throughput_of": "的吞吐量"
"throughput_of": "的吞吐量",
"waiting_for": "等待足够的记录以显示"
},
"auth": {
"login": "请登入你的账户",
"reset": "输入邮箱来重设密码",
"create": "请创建管理员账户",
"create_account": "创建账户",
"sign_in": "登入",
"openid_des": "Beszel 支持 OpenID Connect 和许多 OAuth2 认证提供商。",
"please_view_the": "请检视",
"for_instructions": "以获取更多信息。",
"forgot_password": "忘记密码?",
"reset_password": "重设密码",
"command_line_instructions": "了解命令行指令",
"command_1": "如果您忘记了管理员账户的密码,可以使用以下命令重置。",
"command_2": "然后登录到后台,在用户表中重置您的用户账户密码。"
}
}

View File

@@ -173,6 +173,22 @@
"temperature_des": "系統感應器的溫度",
"usage": "使用率",
"disk_usage_of": "的磁碟使用率",
"throughput_of": "的吞吐量"
"throughput_of": "的吞吐量",
"waiting_for": "等待足夠的記錄以顯示"
},
"auth": {
"login": "請登入你的賬戶",
"reset": "輸入電郵來重設密碼",
"create": "請創建管理員賬戶",
"create_account": "創建賬戶",
"sign_in": "登入",
"openid_des": "Beszel 支持 OpenID Connect 和許多 OAuth2 認證提供商。",
"please_view_the": "請檢視",
"for_instructions": "以獲取更多信息。",
"forgot_password": "忘記密碼?",
"reset_password": "重設密碼",
"command_line_instructions": "了解命令行指令",
"command_1": "如果您忘記了管理員賬戶的密碼,可以使用以下命令重置。",
"command_2": "然後登入到後台,在用戶表中重置您的用戶賬戶密碼。"
}
}