feat: implement node removal
This commit is contained in:
parent
1d9f4553eb
commit
a47fb61549
@ -11,7 +11,7 @@ type Properties = {
|
||||
|
||||
function Dropdown(properties: Properties) {
|
||||
return (
|
||||
<div className='relative'>
|
||||
<div className={clsx('relative', properties.className)}>
|
||||
<Menu>
|
||||
<Button className='flex flex-col items-center'>
|
||||
{properties.button}
|
||||
@ -26,10 +26,10 @@ function Dropdown(properties: Properties) {
|
||||
leaveTo='transform opacity-0 scale-95'
|
||||
>
|
||||
<Menu.Items className={clsx(
|
||||
'absolute right-0 w-36 mt-2 rounded-md',
|
||||
'absolute right-0 w-fit max-w-36 mt-2 rounded-md',
|
||||
'text-gray-700 dark:text-gray-300',
|
||||
'bg-white dark:bg-zinc-800 text-right',
|
||||
'overflow-hidden',
|
||||
'overflow-hidden z-50',
|
||||
'border border-gray-200 dark:border-zinc-700',
|
||||
'divide-y divide-gray-200 dark:divide-zinc-700'
|
||||
)}
|
||||
|
||||
@ -1,29 +1,77 @@
|
||||
/* eslint-disable unicorn/filename-case */
|
||||
import { ClipboardIcon } from '@heroicons/react/24/outline'
|
||||
import { type LoaderFunctionArgs } from '@remix-run/node'
|
||||
import { Link, useLoaderData } from '@remix-run/react'
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
|
||||
import { ClipboardIcon, EllipsisHorizontalIcon } from '@heroicons/react/24/outline'
|
||||
import { type ActionFunctionArgs, json, type LoaderFunctionArgs } from '@remix-run/node'
|
||||
import { Link, useFetcher, useLoaderData } from '@remix-run/react'
|
||||
import clsx from 'clsx'
|
||||
import { useState } from 'react'
|
||||
import { toast } from 'react-hot-toast/headless'
|
||||
|
||||
import Dropdown from '~/components/Dropdown'
|
||||
import useModal from '~/components/Modal'
|
||||
import StatusCircle from '~/components/StatusCircle'
|
||||
import { type Machine } from '~/types'
|
||||
import { pull } from '~/utils/headscale'
|
||||
import { del, 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')!)
|
||||
return data.nodes
|
||||
}
|
||||
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
const data = await request.json() as { id?: string }
|
||||
if (!data.id) {
|
||||
return json({ message: 'No ID provided' }, {
|
||||
status: 400
|
||||
})
|
||||
}
|
||||
|
||||
const session = await getSession(request.headers.get('Cookie'))
|
||||
if (!session.has('hsApiKey')) {
|
||||
return json({ message: 'Unauthorized' }, {
|
||||
status: 401
|
||||
})
|
||||
}
|
||||
|
||||
await del(`v1/node/${data.id}`, session.get('hsApiKey')!)
|
||||
return json({ message: 'Machine removed' })
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
const data = useLoaderData<typeof loader>()
|
||||
useLiveData({ interval: 3000 })
|
||||
const data = useLoaderData<typeof loader>()
|
||||
const [activeId, setActiveId] = useState<string | undefined>(undefined)
|
||||
const fetcher = useFetcher()
|
||||
|
||||
const { Modal, open } = useModal({
|
||||
title: 'Remove Machine',
|
||||
description: [
|
||||
'This action is irreversible and will disconnect the machine from the Headscale server.',
|
||||
'All data associated with this machine including ACLs and tags will be lost.'
|
||||
].join('\n'),
|
||||
variant: 'danger',
|
||||
buttonText: 'Remove',
|
||||
onConfirm: () => {
|
||||
fetcher.submit(
|
||||
{
|
||||
id: activeId!
|
||||
},
|
||||
{
|
||||
method: 'DELETE',
|
||||
encType: 'application/json'
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
{Modal}
|
||||
<table className='table-auto w-full rounded-lg'>
|
||||
<thead className='text-gray-500 dark:text-gray-400'>
|
||||
<tr className='text-left uppercase text-sm font-bold'>
|
||||
@ -39,9 +87,8 @@ export default function Page() {
|
||||
>
|
||||
{data.map(machine => {
|
||||
const tags = [...machine.forcedTags, ...machine.validTags]
|
||||
|
||||
return (
|
||||
<tr key={machine.id} className='hover:bg-zinc-100 dark:hover:bg-zinc-800'>
|
||||
<tr key={machine.id} className='hover:bg-zinc-100 dark:hover:bg-zinc-800 group'>
|
||||
<td className='py-2 pl-4'>
|
||||
<Link to={`/machines/${machine.id}`}>
|
||||
<h1>{machine.givenName}</h1>
|
||||
@ -89,10 +136,36 @@ export default function Page() {
|
||||
</p>
|
||||
</span>
|
||||
</td>
|
||||
<td className='py-2 pr-4'>
|
||||
<div className={clsx(
|
||||
'border border-transparent rounded-lg py-0.5 w-10',
|
||||
'group-hover:border-gray-200 dark:group-hover:border-zinc-700'
|
||||
)}
|
||||
>
|
||||
<Dropdown
|
||||
className='left-1/4 w-min'
|
||||
button={(
|
||||
<EllipsisHorizontalIcon className='w-5 h-5'/>
|
||||
)}
|
||||
>
|
||||
<Dropdown.Item className='text-red-700'>
|
||||
<button
|
||||
type='button' onClick={() => {
|
||||
setActiveId(machine.id)
|
||||
open()
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</Dropdown.Item>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -48,3 +48,18 @@ export async function post<T>(url: string, key: string, body?: unknown) {
|
||||
return (response.json() as Promise<T>)
|
||||
}
|
||||
|
||||
export async function del<T>(url: string, key: string) {
|
||||
const prefix = process.env.HEADSCALE_URL!
|
||||
const response = await fetch(`${prefix}/api/${url}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Authorization: `Bearer ${key}`
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new HeadscaleError(await response.text(), response.status)
|
||||
}
|
||||
|
||||
return (response.json() as Promise<T>)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user