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

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>
)
}