mirror of
https://github.com/fankes/beszel.git
synced 2025-10-19 09:49:28 +08:00
login and other updates
This commit is contained in:
@@ -88,7 +88,6 @@ export default function ({ chartData }: { chartData: Record<string, number | str
|
|||||||
{Object.keys(chartConfig).map((key) => (
|
{Object.keys(chartConfig).map((key) => (
|
||||||
<Area
|
<Area
|
||||||
key={key}
|
key={key}
|
||||||
// isAnimationActive={false}
|
|
||||||
animateNewValues={false}
|
animateNewValues={false}
|
||||||
dataKey={key}
|
dataKey={key}
|
||||||
type="monotone"
|
type="monotone"
|
||||||
|
@@ -51,10 +51,11 @@ export default function ({ chartData }: { chartData: { time: string; cpu: number
|
|||||||
/>
|
/>
|
||||||
<Area
|
<Area
|
||||||
dataKey="cpu"
|
dataKey="cpu"
|
||||||
type="monotone"
|
type="natural"
|
||||||
fill="var(--color-cpu)"
|
fill="var(--color-cpu)"
|
||||||
fillOpacity={0.4}
|
fillOpacity={0.4}
|
||||||
stroke="var(--color-cpu)"
|
stroke="var(--color-cpu)"
|
||||||
|
animateNewValues={false}
|
||||||
/>
|
/>
|
||||||
</AreaChart>
|
</AreaChart>
|
||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
|
@@ -1,22 +1,24 @@
|
|||||||
import { UserAuthForm } from '@/components/user-auth-form'
|
import { UserAuthForm } from '@/components/user-auth-form'
|
||||||
import { Logo } from './logo'
|
import { Logo } from './logo'
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
export default function () {
|
export default function () {
|
||||||
|
useEffect(() => {
|
||||||
|
document.title = 'Login / Qoma'
|
||||||
|
}, [])
|
||||||
return (
|
return (
|
||||||
<div className="relative h-screen grid lg:max-w-none lg:grid-cols-2 lg:px-0">
|
<div className="relative h-screen grid lg:max-w-none lg:px-0">
|
||||||
<div className="grid items-center">
|
<div className="grid items-center py-12">
|
||||||
<div className="flex flex-col justify-center space-y-6 w-full px-4 max-w-[22em] mx-auto">
|
<div className="grid gap-5 w-full px-4 max-w-[22em] mx-auto">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h1 className="mb-4">
|
<h1 className="mb-3">
|
||||||
<Logo className="h-6 fill-foreground mx-auto" />
|
<Logo className="h-7 fill-foreground mx-auto" />
|
||||||
<div className="sr-only">Qoma</div>
|
<span className="sr-only">Qoma</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">Please sign in to your account</p>
|
||||||
Enter your email to sign in to your account
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<UserAuthForm />
|
<UserAuthForm />
|
||||||
<p className="px-8 text-center text-sm text-muted-foreground">
|
<p className="text-center text-sm opacity-70 hover:opacity-100 transition-opacity">
|
||||||
{/* todo: add forgot password section to readme and link to section
|
{/* todo: add forgot password section to readme and link to section
|
||||||
reset w/ command or link to pb reset */}
|
reset w/ command or link to pb reset */}
|
||||||
<a
|
<a
|
||||||
@@ -28,12 +30,12 @@ export default function () {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative hidden h-full bg-primary lg:block">
|
{/* <div className="relative hidden h-full bg-primary lg:block">
|
||||||
<img
|
<img
|
||||||
className="absolute inset-0 h-full w-full object-cover bg-primary"
|
className="absolute inset-0 h-full w-full object-cover bg-primary"
|
||||||
src="/penguin-and-egg.avif"
|
src="/penguin-and-egg.avif"
|
||||||
></img>
|
></img>
|
||||||
</div>
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@@ -1,48 +1,12 @@
|
|||||||
import { Suspense, lazy, useEffect } from 'react'
|
import { Suspense, lazy, useEffect } from 'react'
|
||||||
import { $servers, pb } from '@/lib/stores'
|
|
||||||
// import { DataTable } from '../server-table/data-table'
|
// import { DataTable } from '../server-table/data-table'
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card'
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card'
|
||||||
import { SystemRecord } from '@/types'
|
|
||||||
import { updateServerList } from '@/lib/utils'
|
|
||||||
|
|
||||||
const DataTable = lazy(() => import('../server-table/data-table'))
|
const DataTable = lazy(() => import('../server-table/data-table'))
|
||||||
|
|
||||||
export default function () {
|
export default function () {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = 'Qoma Dashboard'
|
document.title = 'Dashboard / Qoma'
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(updateServerList, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
pb.collection<SystemRecord>('systems').subscribe('*', (e) => {
|
|
||||||
const curServers = $servers.get()
|
|
||||||
const newServers = []
|
|
||||||
console.log('e', e)
|
|
||||||
if (e.action === 'delete') {
|
|
||||||
for (const server of curServers) {
|
|
||||||
if (server.id !== e.record.id) {
|
|
||||||
newServers.push(server)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let found = 0
|
|
||||||
for (const server of curServers) {
|
|
||||||
if (server.id === e.record.id) {
|
|
||||||
found = newServers.push(e.record)
|
|
||||||
} else {
|
|
||||||
newServers.push(server)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!found) {
|
|
||||||
newServers.push(e.record)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$servers.set(newServers)
|
|
||||||
})
|
|
||||||
return () => {
|
|
||||||
pb.collection('systems').unsubscribe('*')
|
|
||||||
}
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@@ -71,7 +71,7 @@ export default function ServerDetail({ name }: { name: string }) {
|
|||||||
// console.log('sctats', records)
|
// console.log('sctats', records)
|
||||||
setServerStats(records.items)
|
setServerStats(records.items)
|
||||||
})
|
})
|
||||||
}, [server])
|
}, [server, servers])
|
||||||
|
|
||||||
// get cpu data
|
// get cpu data
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -100,16 +100,9 @@ export default function ServerDetail({ name }: { name: string }) {
|
|||||||
}
|
}
|
||||||
// console.log('running')
|
// console.log('running')
|
||||||
const matchingServer = servers.find((s) => s.name === name) as SystemRecord
|
const matchingServer = servers.find((s) => s.name === name) as SystemRecord
|
||||||
|
// console.log('found server', matchingServer)
|
||||||
setServer(matchingServer)
|
setServer(matchingServer)
|
||||||
|
|
||||||
console.log('found server', matchingServer)
|
|
||||||
// pb.collection<SystemRecord>('systems')
|
|
||||||
// .getOne(serverId)
|
|
||||||
// .then((record) => {
|
|
||||||
// setServer(record)
|
|
||||||
// })
|
|
||||||
|
|
||||||
pb.collection<ContainerStatsRecord>('container_stats')
|
pb.collection<ContainerStatsRecord>('container_stats')
|
||||||
.getList(1, 60, {
|
.getList(1, 60, {
|
||||||
filter: `system="${matchingServer.id}"`,
|
filter: `system="${matchingServer.id}"`,
|
||||||
|
@@ -62,6 +62,14 @@ import { $servers, pb, navigate } from '@/lib/stores'
|
|||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
import { AddServerButton } from '../add-server'
|
import { AddServerButton } from '../add-server'
|
||||||
import { cn, copyToClipboard } from '@/lib/utils'
|
import { cn, copyToClipboard } from '@/lib/utils'
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogTrigger,
|
||||||
|
DialogDescription,
|
||||||
|
DialogTitle,
|
||||||
|
DialogHeader,
|
||||||
|
} from '@/components/ui/dialog'
|
||||||
|
|
||||||
function CellFormatter(info: CellContext<SystemRecord, unknown>) {
|
function CellFormatter(info: CellContext<SystemRecord, unknown>) {
|
||||||
const val = info.getValue() as number
|
const val = info.getValue() as number
|
||||||
@@ -121,6 +129,7 @@ export default function () {
|
|||||||
style={{ marginBottom: '-1px' }}
|
style={{ marginBottom: '-1px' }}
|
||||||
></span>
|
></span>
|
||||||
<Button
|
<Button
|
||||||
|
data-nolink
|
||||||
variant={'ghost'}
|
variant={'ghost'}
|
||||||
className="text-foreground/80 h-7 px-1.5 gap-1.5"
|
className="text-foreground/80 h-7 px-1.5 gap-1.5"
|
||||||
onClick={() => copyToClipboard(info.getValue() as string)}
|
onClick={() => copyToClipboard(info.getValue() as string)}
|
||||||
@@ -156,17 +165,27 @@ export default function () {
|
|||||||
const { id, name, status, host } = row.original
|
const { id, name, status, host } = row.original
|
||||||
return (
|
return (
|
||||||
<div className={'flex justify-end items-center gap-1'}>
|
<div className={'flex justify-end items-center gap-1'}>
|
||||||
<Button
|
<Dialog>
|
||||||
variant="ghost"
|
<DialogTrigger asChild>
|
||||||
size={'icon'}
|
<Button variant="ghost" size={'icon'} aria-label="Notifications" data-nolink>
|
||||||
onClick={() => alert('notifications coming soon')}
|
<BellIcon className="h-[1.2em] w-[1.2em] pointer-events-none" />
|
||||||
>
|
</Button>
|
||||||
<BellIcon className="h-[1.2em] w-[1.2em] pointer-events-none" />
|
</DialogTrigger>
|
||||||
</Button>
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Notifications</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogDescription>
|
||||||
|
The agent must be running on the server to connect. Copy the{' '}
|
||||||
|
<code className="bg-muted px-1 rounded-sm">docker-compose.yml</code> for the
|
||||||
|
agent below.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="ghost" size={'icon'}>
|
<Button variant="ghost" size={'icon'} data-nolink>
|
||||||
<span className="sr-only">Open menu</span>
|
<span className="sr-only">Open menu</span>
|
||||||
<MoreHorizontal className="w-5" />
|
<MoreHorizontal className="w-5" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -301,7 +320,7 @@ export default function () {
|
|||||||
})}
|
})}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
const target = e.target as HTMLElement
|
const target = e.target as HTMLElement
|
||||||
if (target.tagName !== 'BUTTON' && !target.hasAttribute('role')) {
|
if (!target.closest('[data-nolink]') && e.currentTarget.contains(target)) {
|
||||||
navigate(`/server/${row.original.name}`)
|
navigate(`/server/${row.original.name}`)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
@@ -8,78 +8,88 @@ import * as React from 'react'
|
|||||||
// import * as z from 'zod'
|
// import * as z from 'zod'
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { userAuthSchema } from '@/lib/validations/auth'
|
|
||||||
import { buttonVariants } from '@/components/ui/button'
|
import { buttonVariants } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
// import { toast } from '@/components/ui/use-toast'
|
// import { toast } from '@/components/ui/use-toast'
|
||||||
import { Github, LoaderCircle } from 'lucide-react'
|
import { Github, LoaderCircle, LogInIcon } from 'lucide-react'
|
||||||
|
import { pb } from '@/lib/stores'
|
||||||
|
import * as v from 'valibot'
|
||||||
|
import { toast } from './ui/use-toast'
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogTrigger,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog'
|
||||||
|
|
||||||
interface UserAuthFormProps extends React.HTMLAttributes<HTMLDivElement> {}
|
const LoginSchema = v.object({
|
||||||
|
email: v.pipe(v.string(), v.email('Invalid email address.')),
|
||||||
|
password: v.pipe(v.string(), v.minLength(10, 'Password must be at least 10 characters long.')),
|
||||||
|
})
|
||||||
|
|
||||||
type FormData = z.infer<typeof userAuthSchema>
|
// type LoginData = v.InferOutput<typeof LoginSchema> // { email: string; password: string }
|
||||||
|
|
||||||
export function UserAuthForm({ className, ...props }: UserAuthFormProps) {
|
export function UserAuthForm({ className, ...props }: { className?: string }) {
|
||||||
const signIn = (s: string) => console.log(s)
|
|
||||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
|
||||||
// e.preventDefault()
|
|
||||||
signIn('github')
|
|
||||||
}
|
|
||||||
|
|
||||||
const errors = {
|
|
||||||
email: 'This field is required',
|
|
||||||
password: 'This field is required',
|
|
||||||
}
|
|
||||||
|
|
||||||
// const {
|
|
||||||
// register,
|
|
||||||
// handleSubmit,
|
|
||||||
// formState: { errors },
|
|
||||||
// } = useForm<FormData>({
|
|
||||||
// resolver: zodResolver(userAuthSchema),
|
|
||||||
// })
|
|
||||||
const [isLoading, setIsLoading] = React.useState<boolean>(false)
|
const [isLoading, setIsLoading] = React.useState<boolean>(false)
|
||||||
const [isGitHubLoading, setIsGitHubLoading] = React.useState<boolean>(false)
|
const [isGitHubLoading, setIsGitHubLoading] = React.useState<boolean>(false)
|
||||||
|
const [errors, setErrors] = React.useState<Record<string, string | undefined>>({})
|
||||||
|
|
||||||
// const searchParams = useSearchParams()
|
// const searchParams = useSearchParams()
|
||||||
|
|
||||||
async function onSubmit(data: FormData) {
|
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||||
|
e.preventDefault()
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
|
try {
|
||||||
alert('do pb stuff')
|
const formData = new FormData(e.target as HTMLFormElement)
|
||||||
|
const data = Object.fromEntries(formData) as Record<string, any>
|
||||||
// const signInResult = await signIn('email', {
|
const result = v.safeParse(LoginSchema, data)
|
||||||
// email: data.email.toLowerCase(),
|
if (!result.success) {
|
||||||
// redirect: false,
|
let errors = {}
|
||||||
// callbackUrl: searchParams?.get('from') || '/dashboard',
|
for (const issue of result.issues) {
|
||||||
// })
|
// @ts-ignore
|
||||||
|
errors[issue.path[0].key] = issue.message
|
||||||
setIsLoading(false)
|
}
|
||||||
|
setErrors(errors)
|
||||||
if (!signInResult?.ok) {
|
return
|
||||||
alert('Your sign in request failed. Please try again.')
|
}
|
||||||
// return toast({
|
const { email, password } = result.output
|
||||||
// title: 'Something went wrong.',
|
let firstRun = true
|
||||||
// description: 'Your sign in request failed. Please try again.',
|
if (firstRun) {
|
||||||
// variant: 'destructive',
|
await pb.admins.create({
|
||||||
// })
|
email,
|
||||||
|
password,
|
||||||
|
passwordConfirm: password,
|
||||||
|
})
|
||||||
|
await pb.admins.authWithPassword(email, password)
|
||||||
|
} else {
|
||||||
|
await pb.admins.authWithPassword(email, password)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return toast({
|
||||||
|
title: 'Login attempt failed',
|
||||||
|
description: 'Please check your credentials and try again',
|
||||||
|
variant: 'destructive',
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// return toast({
|
|
||||||
// title: 'Check your email',
|
|
||||||
// description: 'We sent you a login link. Be sure to check your spam too.',
|
|
||||||
// })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('grid gap-6', className)} {...props}>
|
<div className={cn('grid gap-6', className)} {...props}>
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2.5">
|
||||||
<div className="grid gap-1">
|
<div className="grid gap-1">
|
||||||
<Label className="sr-only" htmlFor="email">
|
<Label className="sr-only" htmlFor="email">
|
||||||
Email
|
Email
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="email"
|
id="email"
|
||||||
|
name="email"
|
||||||
|
required
|
||||||
placeholder="name@example.com"
|
placeholder="name@example.com"
|
||||||
type="email"
|
type="email"
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
@@ -87,11 +97,30 @@ export function UserAuthForm({ className, ...props }: UserAuthFormProps) {
|
|||||||
autoCorrect="off"
|
autoCorrect="off"
|
||||||
disabled={isLoading || isGitHubLoading}
|
disabled={isLoading || isGitHubLoading}
|
||||||
/>
|
/>
|
||||||
{errors?.email && <p className="px-1 text-xs text-red-600">{errors.email.message}</p>}
|
{errors?.email && <p className="px-1 text-xs text-red-600">{errors.email}</p>}
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Label className="sr-only" htmlFor="email">
|
||||||
|
Email
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="pass"
|
||||||
|
name="password"
|
||||||
|
placeholder="password"
|
||||||
|
required
|
||||||
|
type="password"
|
||||||
|
autoComplete="current-password"
|
||||||
|
disabled={isLoading || isGitHubLoading}
|
||||||
|
/>
|
||||||
|
{errors?.password && <p className="px-1 text-xs text-red-600">{errors.password}</p>}
|
||||||
</div>
|
</div>
|
||||||
<button className={cn(buttonVariants())} disabled={isLoading}>
|
<button className={cn(buttonVariants())} disabled={isLoading}>
|
||||||
{isLoading && <LoaderCircle className="mr-2 h-4 w-4 animate-spin" />}
|
{isLoading ? (
|
||||||
Sign In with Email
|
<LoaderCircle className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<LogInIcon className="mr-2 h-4 w-4" />
|
||||||
|
)}
|
||||||
|
Sign In
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -103,23 +132,35 @@ export function UserAuthForm({ className, ...props }: UserAuthFormProps) {
|
|||||||
<span className="bg-background px-2 text-muted-foreground">Or continue with</span>
|
<span className="bg-background px-2 text-muted-foreground">Or continue with</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<Dialog>
|
||||||
type="button"
|
<DialogTrigger asChild>
|
||||||
className={cn(buttonVariants({ variant: 'outline' }))}
|
<button
|
||||||
onClick={() => {
|
type="button"
|
||||||
localStorage.setItem('auth', 'true')
|
className={cn(buttonVariants({ variant: 'outline' }))}
|
||||||
setIsGitHubLoading(true)
|
// onClick={async () => {
|
||||||
signIn('github')
|
// setIsGitHubLoading(true)
|
||||||
}}
|
// do stuff
|
||||||
disabled={isLoading || isGitHubLoading}
|
// setIsGitHubLoading(false)
|
||||||
>
|
// }}
|
||||||
{isGitHubLoading ? (
|
disabled={isLoading || isGitHubLoading}
|
||||||
<LoaderCircle className="mr-2 h-4 w-4 animate-spin" />
|
>
|
||||||
) : (
|
{isGitHubLoading ? (
|
||||||
<Github className="mr-2 h-4 w-4" />
|
<LoaderCircle className="mr-2 h-4 w-4 animate-spin" />
|
||||||
)}{' '}
|
) : (
|
||||||
Github
|
<Github className="mr-2 h-4 w-4" />
|
||||||
</button>
|
)}{' '}
|
||||||
|
Github
|
||||||
|
</button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-[425px]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>OAuth support coming soon</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
OAuth / OpenID with all major providers should be available at 1.0.0.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@@ -24,6 +24,7 @@ import {
|
|||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from './components/ui/dropdown-menu.tsx'
|
} from './components/ui/dropdown-menu.tsx'
|
||||||
|
import { SystemRecord } from './types'
|
||||||
|
|
||||||
const ServerDetail = lazy(() => import('./components/routes/server.tsx'))
|
const ServerDetail = lazy(() => import('./components/routes/server.tsx'))
|
||||||
const CommandPalette = lazy(() => import('./components/command-palette.tsx'))
|
const CommandPalette = lazy(() => import('./components/command-palette.tsx'))
|
||||||
@@ -37,27 +38,53 @@ const App = () => {
|
|||||||
// get servers
|
// get servers
|
||||||
useEffect(updateServerList, [])
|
useEffect(updateServerList, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
pb.collection<SystemRecord>('systems').subscribe('*', (e) => {
|
||||||
|
const curServers = $servers.get()
|
||||||
|
const newServers = []
|
||||||
|
// console.log('e', e)
|
||||||
|
if (e.action === 'delete') {
|
||||||
|
for (const server of curServers) {
|
||||||
|
if (server.id !== e.record.id) {
|
||||||
|
newServers.push(server)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let found = 0
|
||||||
|
for (const server of curServers) {
|
||||||
|
if (server.id === e.record.id) {
|
||||||
|
found = newServers.push(e.record)
|
||||||
|
} else {
|
||||||
|
newServers.push(server)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
newServers.push(e.record)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$servers.set(newServers)
|
||||||
|
})
|
||||||
|
return () => {
|
||||||
|
pb.collection('systems').unsubscribe('*')
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
// update favicon
|
// update favicon
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!authenticated || !servers.length) {
|
if (!authenticated || !servers.length) {
|
||||||
console.log('no auth favicon')
|
|
||||||
updateFavicon('/favicon.svg')
|
updateFavicon('/favicon.svg')
|
||||||
} else {
|
} else {
|
||||||
const cleanup = () => {
|
|
||||||
updateFavicon('/favicon.svg')
|
|
||||||
}
|
|
||||||
let up = false
|
let up = false
|
||||||
for (const server of servers) {
|
for (const server of servers) {
|
||||||
if (server.status === 'down') {
|
if (server.status === 'down') {
|
||||||
console.log('down', server)
|
|
||||||
updateFavicon('/favicon-red.svg')
|
updateFavicon('/favicon-red.svg')
|
||||||
return cleanup
|
return () => updateFavicon('/favicon.svg')
|
||||||
} else if (server.status === 'up') {
|
} else if (server.status === 'up') {
|
||||||
up = true
|
up = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
updateFavicon(up ? '/favicon-green.svg' : '/favicon.svg')
|
updateFavicon(up ? '/favicon-green.svg' : '/favicon.svg')
|
||||||
return cleanup
|
return () => updateFavicon('/favicon.svg')
|
||||||
}
|
}
|
||||||
return () => {
|
return () => {
|
||||||
updateFavicon('/favicon.svg')
|
updateFavicon('/favicon.svg')
|
||||||
@@ -155,7 +182,6 @@ const Layout = () => {
|
|||||||
<div className="container mb-14 relative">
|
<div className="container mb-14 relative">
|
||||||
<App />
|
<App />
|
||||||
<CommandPalette />
|
<CommandPalette />
|
||||||
<Toaster />
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
@@ -167,6 +193,9 @@ ReactDOM.createRoot(document.getElementById('app')!).render(
|
|||||||
<Suspense>
|
<Suspense>
|
||||||
<Layout />
|
<Layout />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
<Suspense>
|
||||||
|
<Toaster />
|
||||||
|
</Suspense>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
)
|
)
|
||||||
|
Reference in New Issue
Block a user