mirror of
https://github.com/tale/headplane.git
synced 2026-08-28 16:07:07 +00:00
feat: add user page
This commit is contained in:
@@ -10,8 +10,8 @@ type Properties = {
|
|||||||
export default function Attribute({ name, value, isCopyable }: Properties) {
|
export default function Attribute({ name, value, isCopyable }: Properties) {
|
||||||
const canCopy = isCopyable ?? false
|
const canCopy = isCopyable ?? false
|
||||||
return (
|
return (
|
||||||
<dl className='flex gap-1 text-sm'>
|
<dl className='flex gap-1 text-sm w-full'>
|
||||||
<dt className='w-1/4 shrink-0 min-w-0 truncate text-gray-700 dark:text-gray-300'>
|
<dt className='w-1/4 shrink-0 min-w-0 truncate text-gray-700 dark:text-gray-300 py-1'>
|
||||||
{name}
|
{name}
|
||||||
</dt>
|
</dt>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CpuChipIcon, ServerStackIcon } from '@heroicons/react/24/outline'
|
import { CpuChipIcon, ServerStackIcon, UsersIcon } from '@heroicons/react/24/outline'
|
||||||
import { type LoaderFunctionArgs, redirect } from '@remix-run/node'
|
import { type LoaderFunctionArgs, redirect } from '@remix-run/node'
|
||||||
import { Outlet } from '@remix-run/react'
|
import { Outlet } from '@remix-run/react'
|
||||||
|
|
||||||
@@ -46,6 +46,7 @@ export default function Layout() {
|
|||||||
</div>
|
</div>
|
||||||
<div className='flex items-center gap-x-4'>
|
<div className='flex items-center gap-x-4'>
|
||||||
<TabLink to='/machines' name='Machines' icon={<ServerStackIcon className='w-4 h-4'/>}/>
|
<TabLink to='/machines' name='Machines' icon={<ServerStackIcon className='w-4 h-4'/>}/>
|
||||||
|
<TabLink to='/users' name='Users' icon={<UsersIcon className='w-4 h-4'/>}/>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
/* eslint-disable unicorn/filename-case */
|
||||||
|
import { ClipboardIcon, UserIcon } from '@heroicons/react/24/outline'
|
||||||
|
import { type LoaderFunctionArgs } from '@remix-run/node'
|
||||||
|
import { useLoaderData } from '@remix-run/react'
|
||||||
|
import { toast } from 'react-hot-toast/headless'
|
||||||
|
|
||||||
|
import Attribute from '~/components/Attribute'
|
||||||
|
import StatusCircle from '~/components/StatusCircle'
|
||||||
|
import { type Machine, type User } from '~/types'
|
||||||
|
import { pull } from '~/utils/headscale'
|
||||||
|
import { getSession } from '~/utils/sessions'
|
||||||
|
import { useLiveData } from '~/utils/useLiveData'
|
||||||
|
|
||||||
|
export async function loader({ request }: LoaderFunctionArgs) {
|
||||||
|
const session = await getSession(request.headers.get('Cookie'))
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
const data = await pull<{ nodes: Machine[] }>('v1/node', session.get('hsApiKey')!)
|
||||||
|
|
||||||
|
const users = new Map<string, Machine[]>()
|
||||||
|
for (const machine of data.nodes) {
|
||||||
|
const { user } = machine
|
||||||
|
if (!users.has(user.id)) {
|
||||||
|
users.set(user.id, [])
|
||||||
|
}
|
||||||
|
|
||||||
|
users.get(user.id)?.push(machine)
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...users.values()].map(machines => {
|
||||||
|
const { user } = machines[0]
|
||||||
|
|
||||||
|
return {
|
||||||
|
...user,
|
||||||
|
machines
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
const data = useLoaderData<typeof loader>()
|
||||||
|
useLiveData({ interval: 3000 })
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='grid grid-cols-2 gap-4 auto-rows-min'>
|
||||||
|
{data.map(user => (
|
||||||
|
<div key={user.id} className='border rounded-lg p-4'>
|
||||||
|
<div className='flex items-center gap-4'>
|
||||||
|
<UserIcon className='w-6 h-6'/>
|
||||||
|
<span className='text-lg font-mono'>
|
||||||
|
{user.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className='py-4'>
|
||||||
|
{user.machines.map(machine => (
|
||||||
|
<div key={machine.id} className='flex items-center w-full gap-4'>
|
||||||
|
<StatusCircle isOnline={machine.online} className='w-4 h-4 px-1 w-fit'/>
|
||||||
|
<Attribute name={`Node ${machine.id}`} value={machine.givenName}/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user