mirror of
https://github.com/henrygd/beszel.git
synced 2025-12-17 10:46:16 +01:00
login i18n & chart loading & fix forgot pass page (#236)
Co-authored-by: hank <hank@henrygd.me>
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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])
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ export const $router = createRouter(
|
||||
home: '/',
|
||||
server: '/system/:name',
|
||||
settings: '/settings/:name?',
|
||||
forgot_password: '/forgot-password',
|
||||
},
|
||||
{ links: false }
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user