mirror of
https://github.com/fankes/beszel.git
synced 2025-10-19 09:49:28 +08:00
web ui design updates
This commit is contained in:
@@ -13,12 +13,13 @@ import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '@/comp
|
|||||||
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 { $publicKey, pb } from '@/lib/stores'
|
import { $publicKey, pb } from '@/lib/stores'
|
||||||
import { Copy, Plus } from 'lucide-react'
|
import { Copy, PlusIcon } from 'lucide-react'
|
||||||
import { useState, useRef, MutableRefObject } from 'react'
|
import { useState, useRef, MutableRefObject } from 'react'
|
||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
import { copyToClipboard } from '@/lib/utils'
|
import { cn, copyToClipboard, isReadOnlyUser } from '@/lib/utils'
|
||||||
|
import { navigate } from './router'
|
||||||
|
|
||||||
export function AddSystemButton() {
|
export function AddSystemButton({ className }: { className?: string }) {
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const port = useRef() as MutableRefObject<HTMLInputElement>
|
const port = useRef() as MutableRefObject<HTMLInputElement>
|
||||||
const publicKey = useStore($publicKey)
|
const publicKey = useStore($publicKey)
|
||||||
@@ -46,6 +47,7 @@ export function AddSystemButton() {
|
|||||||
try {
|
try {
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
await pb.collection('systems').create(data)
|
await pb.collection('systems').create(data)
|
||||||
|
navigate('/')
|
||||||
// console.log(record)
|
// console.log(record)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e)
|
console.log(e)
|
||||||
@@ -55,8 +57,11 @@ export function AddSystemButton() {
|
|||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="outline" className="flex gap-1">
|
<Button
|
||||||
<Plus className="h-4 w-4 mr-auto" />
|
variant="outline"
|
||||||
|
className={cn('flex gap-1 max-xs:h-[2.4rem]', className, isReadOnlyUser() && 'hidden')}
|
||||||
|
>
|
||||||
|
<PlusIcon className="h-4 w-4 -ml-1" />
|
||||||
Add <span className="hidden sm:inline">System</span>
|
Add <span className="hidden sm:inline">System</span>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
|
@@ -43,7 +43,7 @@ export default function CommandPalette() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<CommandDialog open={open} onOpenChange={setOpen}>
|
<CommandDialog open={open} onOpenChange={setOpen}>
|
||||||
<CommandInput placeholder="Type a command or search..." />
|
<CommandInput placeholder="Search for systems or settings..." />
|
||||||
<CommandList>
|
<CommandList>
|
||||||
<CommandEmpty>No results found.</CommandEmpty>
|
<CommandEmpty>No results found.</CommandEmpty>
|
||||||
<CommandGroup heading="Suggestions">
|
<CommandGroup heading="Suggestions">
|
||||||
|
@@ -15,7 +15,7 @@ export function ModeToggle() {
|
|||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant={'ghost'} size="icon">
|
<Button className="max-sm:w-9" variant={'ghost'} size="icon">
|
||||||
<Sun className="h-[1.2rem] w-[1.2rem] dark:opacity-0" />
|
<Sun className="h-[1.2rem] w-[1.2rem] dark:opacity-0" />
|
||||||
<MoonStarIcon className="absolute h-[1.2rem] w-[1.2rem] opacity-0 dark:opacity-100" />
|
<MoonStarIcon className="absolute h-[1.2rem] w-[1.2rem] opacity-0 dark:opacity-100" />
|
||||||
<span className="sr-only">Toggle theme</span>
|
<span className="sr-only">Toggle theme</span>
|
||||||
|
@@ -1,20 +1,25 @@
|
|||||||
import { Suspense, lazy, useEffect } from 'react'
|
import { Suspense, lazy, useEffect, useState } from 'react'
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card'
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card'
|
||||||
import { $alerts, $hubVersion, $systems, pb } from '@/lib/stores'
|
import { $alerts, $hubVersion, $systems, pb } from '@/lib/stores'
|
||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
import { GithubIcon } from 'lucide-react'
|
import { GithubIcon } from 'lucide-react'
|
||||||
import { Separator } from '../ui/separator'
|
import { Separator } from '../ui/separator'
|
||||||
import { updateRecordList } from '@/lib/utils'
|
import { updateRecordList, updateSystemList } from '@/lib/utils'
|
||||||
import { AlertRecord, SystemRecord } from '@/types'
|
import { AlertRecord, SystemRecord } from '@/types'
|
||||||
|
import { Input } from '../ui/input'
|
||||||
|
|
||||||
const SystemsTable = lazy(() => import('../systems-table/systems-table'))
|
const SystemsTable = lazy(() => import('../systems-table/systems-table'))
|
||||||
|
|
||||||
export default function () {
|
export default function () {
|
||||||
const hubVersion = useStore($hubVersion)
|
const hubVersion = useStore($hubVersion)
|
||||||
|
const [filter, setFilter] = useState<string>()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = 'Dashboard / Beszel'
|
document.title = 'Dashboard / Beszel'
|
||||||
|
|
||||||
|
// make sure we have the latest list of systems
|
||||||
|
updateSystemList()
|
||||||
|
|
||||||
// subscribe to real time updates for systems / alerts
|
// subscribe to real time updates for systems / alerts
|
||||||
pb.collection<SystemRecord>('systems').subscribe('*', (e) => {
|
pb.collection<SystemRecord>('systems').subscribe('*', (e) => {
|
||||||
updateRecordList(e, $systems)
|
updateRecordList(e, $systems)
|
||||||
@@ -31,19 +36,29 @@ export default function () {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="pb-2 md:pb-5 px-4 sm:px-7 max-sm:pt-5">
|
<CardHeader className="pb-5 px-2 sm:px-6 max-sm:pt-5 max-sm:pb-1">
|
||||||
<CardTitle className="mb-1.5">All Systems</CardTitle>
|
<div className="grid md:flex gap-3 w-full items-end">
|
||||||
<CardDescription>
|
<div className="px-2 sm:px-1">
|
||||||
Updated in real time. Press{' '}
|
<CardTitle className="mb-2.5">All Systems</CardTitle>
|
||||||
<kbd className="pointer-events-none inline-flex h-5 select-none items-center gap-0.5 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground opacity-100">
|
<CardDescription>
|
||||||
<span className="text-xs">⌘</span>K
|
Updated in real time. Press{' '}
|
||||||
</kbd>{' '}
|
<kbd className="pointer-events-none inline-flex h-5 select-none items-center gap-0.5 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground opacity-100">
|
||||||
to open the command palette.
|
<span className="text-xs">⌘</span>K
|
||||||
</CardDescription>
|
</kbd>{' '}
|
||||||
|
to open the command palette.
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
// @ts-ignore
|
||||||
|
placeholder="Filter..."
|
||||||
|
onChange={(e) => setFilter(e.target.value)}
|
||||||
|
className="w-full md:w-56 lg:w-80 ml-auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="max-sm:p-2">
|
<CardContent className="max-sm:p-2">
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<SystemsTable />
|
<SystemsTable filter={filter} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
@@ -57,10 +57,9 @@ import {
|
|||||||
Trash2Icon,
|
Trash2Icon,
|
||||||
WifiIcon,
|
WifiIcon,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import { $hubVersion, $systems, pb } from '@/lib/stores'
|
import { $hubVersion, $systems, pb } from '@/lib/stores'
|
||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
import { AddSystemButton } from '../add-system'
|
|
||||||
import { cn, copyToClipboard, isReadOnlyUser } from '@/lib/utils'
|
import { cn, copyToClipboard, isReadOnlyUser } from '@/lib/utils'
|
||||||
import AlertsButton from '../table-alerts'
|
import AlertsButton from '../table-alerts'
|
||||||
import { navigate } from '../router'
|
import { navigate } from '../router'
|
||||||
@@ -102,12 +101,18 @@ function sortableHeader(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SystemsTable() {
|
export default function SystemsTable({ filter }: { filter?: string }) {
|
||||||
const data = useStore($systems)
|
const data = useStore($systems)
|
||||||
const hubVersion = useStore($hubVersion)
|
const hubVersion = useStore($hubVersion)
|
||||||
const [sorting, setSorting] = useState<SortingState>([])
|
const [sorting, setSorting] = useState<SortingState>([])
|
||||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
|
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (filter !== undefined) {
|
||||||
|
table.getColumn('name')?.setFilterValue(filter)
|
||||||
|
}
|
||||||
|
}, [filter])
|
||||||
|
|
||||||
const columns: ColumnDef<SystemRecord>[] = useMemo(() => {
|
const columns: ColumnDef<SystemRecord>[] = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -166,7 +171,7 @@ export default function SystemsTable() {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<span className="flex gap-2 items-center md:pr-5 tabular-nums pl-1.5">
|
<span className="flex gap-2 items-center md:pr-5 tabular-nums pl-1">
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-2 h-2 left-0 rounded-full',
|
'w-2 h-2 left-0 rounded-full',
|
||||||
@@ -278,80 +283,64 @@ export default function SystemsTable() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="rounded-md border overflow-hidden">
|
||||||
<div className="w-full">
|
<Table>
|
||||||
<div className="flex items-center mb-4 gap-2">
|
<TableHeader className="bg-muted/40">
|
||||||
<Input
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
// @ts-ignore
|
<TableRow key={headerGroup.id}>
|
||||||
placeholder="Filter..."
|
{headerGroup.headers.map((header) => {
|
||||||
value={(table.getColumn('name')?.getFilterValue() as string) ?? ''}
|
return (
|
||||||
onChange={(event) => table.getColumn('name')?.setFilterValue(event.target.value)}
|
<TableHead className="px-2" key={header.id}>
|
||||||
className="max-w-sm"
|
{header.isPlaceholder
|
||||||
/>
|
? null
|
||||||
<div className={cn('ml-auto flex gap-2', isReadOnlyUser() && 'hidden')}>
|
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||||
<AddSystemButton />
|
</TableHead>
|
||||||
</div>
|
)
|
||||||
</div>
|
})}
|
||||||
<div className="rounded-md border overflow-hidden">
|
</TableRow>
|
||||||
<Table>
|
))}
|
||||||
<TableHeader className="bg-muted/40">
|
</TableHeader>
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
<TableBody>
|
||||||
<TableRow key={headerGroup.id}>
|
{table.getRowModel().rows?.length ? (
|
||||||
{headerGroup.headers.map((header) => {
|
table.getRowModel().rows.map((row) => (
|
||||||
return (
|
<TableRow
|
||||||
<TableHead className="px-2" key={header.id}>
|
key={row.original.id}
|
||||||
{header.isPlaceholder
|
data-state={row.getIsSelected() && 'selected'}
|
||||||
? null
|
className={cn('cursor-pointer transition-opacity', {
|
||||||
: flexRender(header.column.columnDef.header, header.getContext())}
|
'opacity-50': row.original.status === 'paused',
|
||||||
</TableHead>
|
})}
|
||||||
)
|
onClick={(e) => {
|
||||||
})}
|
const target = e.target as HTMLElement
|
||||||
</TableRow>
|
if (!target.closest('[data-nolink]') && e.currentTarget.contains(target)) {
|
||||||
))}
|
navigate(`/system/${encodeURIComponent(row.original.name)}`)
|
||||||
</TableHeader>
|
}
|
||||||
<TableBody>
|
}}
|
||||||
{table.getRowModel().rows?.length ? (
|
>
|
||||||
table.getRowModel().rows.map((row) => (
|
{row.getVisibleCells().map((cell) => (
|
||||||
<TableRow
|
<TableCell
|
||||||
key={row.original.id}
|
key={cell.id}
|
||||||
data-state={row.getIsSelected() && 'selected'}
|
style={{
|
||||||
className={cn('cursor-pointer transition-opacity', {
|
width:
|
||||||
'opacity-50': row.original.status === 'paused',
|
cell.column.getSize() === Number.MAX_SAFE_INTEGER
|
||||||
})}
|
? 'auto'
|
||||||
onClick={(e) => {
|
: cell.column.getSize(),
|
||||||
const target = e.target as HTMLElement
|
|
||||||
if (!target.closest('[data-nolink]') && e.currentTarget.contains(target)) {
|
|
||||||
navigate(`/system/${encodeURIComponent(row.original.name)}`)
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
|
className={'overflow-hidden relative py-2.5'}
|
||||||
>
|
>
|
||||||
{row.getVisibleCells().map((cell) => (
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
<TableCell
|
|
||||||
key={cell.id}
|
|
||||||
style={{
|
|
||||||
width:
|
|
||||||
cell.column.getSize() === Number.MAX_SAFE_INTEGER
|
|
||||||
? 'auto'
|
|
||||||
: cell.column.getSize(),
|
|
||||||
}}
|
|
||||||
className={'overflow-hidden relative py-2.5'}
|
|
||||||
>
|
|
||||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
||||||
</TableCell>
|
|
||||||
))}
|
|
||||||
</TableRow>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
|
||||||
No systems found
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
))}
|
||||||
)}
|
</TableRow>
|
||||||
</TableBody>
|
))
|
||||||
</Table>
|
) : (
|
||||||
</div>
|
<TableRow>
|
||||||
</div>
|
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||||
</>
|
No systems found
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@@ -28,9 +28,9 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
} from './components/ui/dropdown-menu.tsx'
|
} from './components/ui/dropdown-menu.tsx'
|
||||||
import { AlertRecord, SystemRecord } from './types'
|
|
||||||
import { $router, Link, navigate } from './components/router.tsx'
|
import { $router, Link, navigate } from './components/router.tsx'
|
||||||
import SystemDetail from './components/routes/system.tsx'
|
import SystemDetail from './components/routes/system.tsx'
|
||||||
|
import { AddSystemButton } from './components/add-system.tsx'
|
||||||
|
|
||||||
// const ServerDetail = lazy(() => import('./components/routes/system.tsx'))
|
// const ServerDetail = lazy(() => import('./components/routes/system.tsx'))
|
||||||
const CommandPalette = lazy(() => import('./components/command-palette.tsx'))
|
const CommandPalette = lazy(() => import('./components/command-palette.tsx'))
|
||||||
@@ -101,7 +101,7 @@ const Layout = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<div className="flex items-center h-14 md:h-16 bg-card px-6 border bt-0 rounded-md my-4">
|
<div className="flex items-center h-14 md:h-16 bg-card px-4 pr-3 sm:px-6 border bt-0 rounded-md my-4">
|
||||||
<Link
|
<Link
|
||||||
href="/"
|
href="/"
|
||||||
aria-label="Home"
|
aria-label="Home"
|
||||||
@@ -114,18 +114,18 @@ const Layout = () => {
|
|||||||
<Logo className="h-[1.15em] fill-foreground" />
|
<Logo className="h-[1.15em] fill-foreground" />
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<div className={'flex ml-auto'}>
|
<div className={'flex ml-auto items-center'}>
|
||||||
<ModeToggle />
|
<ModeToggle />
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<button
|
<button
|
||||||
aria-label="User Actions"
|
aria-label="User Actions"
|
||||||
className={cn('', buttonVariants({ variant: 'ghost', size: 'icon' }))}
|
className={cn('max-sm:w-9', buttonVariants({ variant: 'ghost', size: 'icon' }))}
|
||||||
>
|
>
|
||||||
<UserIcon className="h-[1.2rem] w-[1.2rem]" />
|
<UserIcon className="h-[1.2rem] w-[1.2rem]" />
|
||||||
</button>
|
</button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" className="min-w-44">
|
<DropdownMenuContent className="min-w-44">
|
||||||
<DropdownMenuLabel>{pb.authStore.model?.email}</DropdownMenuLabel>
|
<DropdownMenuLabel>{pb.authStore.model?.email}</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
@@ -171,6 +171,7 @@ const Layout = () => {
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
<AddSystemButton className="ml-2" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Reference in New Issue
Block a user