Compare commits

..

4 Commits

Author SHA1 Message Date
Aarnav Tale 6dae5d647a chore: various fixes 2024-04-01 19:41:39 -04:00
Aarnav Tale d787b8517e fix: protect the restart endpoint with auth 2024-04-01 19:33:22 -04:00
Aarnav Tale bdb00b6cd7 feat: make the modal and dropdown more workable 2024-03-31 19:06:06 -04:00
Aarnav Tale f1347803a4 fix: logout is a button 2024-03-31 18:14:28 -04:00
5 changed files with 111 additions and 39 deletions
+6 -4
View File
@@ -7,6 +7,7 @@ type Properties = {
readonly button: ReactNode;
// eslint-disable-next-line unicorn/no-keyword-prefix
readonly className?: string;
readonly width?: string;
}
function Dropdown(properties: Properties) {
@@ -26,12 +27,13 @@ function Dropdown(properties: Properties) {
leaveTo='transform opacity-0 scale-95'
>
<Menu.Items className={clsx(
'absolute right-0 w-fit max-w-36 mt-2 rounded-md',
'absolute right-0 mt-2 rounded-md',
'text-gray-700 dark:text-gray-300',
'bg-white dark:bg-zinc-800 text-right',
'bg-white dark:bg-zinc-800',
'overflow-hidden z-50',
'border border-gray-200 dark:border-zinc-700',
'divide-y divide-gray-200 dark:divide-zinc-700'
'divide-y divide-gray-200 dark:divide-zinc-700',
properties.width ?? 'w-36'
)}
>
{properties.children}
@@ -66,7 +68,7 @@ function Item(properties: ItemProperties) {
<div
{...properties}
className={clsx(
'px-4 py-2 w-full text-right',
'px-4 py-2 w-full',
'focus:outline-none focus:ring',
'focus:ring-gray-300 dark:focus:ring-zinc-700',
properties.className,
+30 -7
View File
@@ -14,36 +14,60 @@ type HookParameters = {
// Optional because the button submits
onConfirm?: () => void | Promise<void>;
onOpen?: () => void | Promise<void>;
onClose?: () => void | Promise<void>;
}
type Overrides = Omit<HookParameters, 'onOpen' | 'onClose'>
type Properties = {
readonly isOpen: boolean;
readonly setIsOpen: (value: SetStateAction<boolean>) => void;
readonly parameters: HookParameters;
readonly parameters?: HookParameters;
}
export default function useModal(properties: HookParameters) {
export default function useModal(properties?: HookParameters) {
const [isOpen, setIsOpen] = useState(false)
const [liveProperties, setLiveProperties] = useState(properties)
return {
Modal: (
<Modal
isOpen={isOpen}
setIsOpen={setIsOpen}
parameters={properties}
parameters={liveProperties}
/>
),
open: () => {
open: (overrides?: Overrides) => {
if (!overrides && !properties) {
throw new Error('No properties provided')
}
setIsOpen(true)
if (properties?.onOpen) {
void properties.onOpen()
}
if (overrides) {
setLiveProperties(overrides)
}
},
close: () => {
setIsOpen(false)
if (properties?.onClose) {
void properties.onClose()
}
}
}
}
function Modal({ parameters, isOpen, setIsOpen }: Properties) {
if (!parameters) {
return
}
return (
<Transition
show={isOpen}
@@ -102,7 +126,7 @@ function Modal({ parameters, isOpen, setIsOpen }: Properties) {
</Dialog.Description>
) : undefined}
{parameters.children ? (
<div className='mt-12 w-full'>
<div className='w-full mt-4'>
{parameters.children}
</div>
) : undefined}
@@ -110,8 +134,7 @@ function Modal({ parameters, isOpen, setIsOpen }: Properties) {
variant='emphasized'
type='submit'
className={clsx(
'w-full',
parameters.children ? 'mt-4' : 'mt-12',
'w-full mt-12',
parameters.variant === 'danger'
? 'bg-red-800 dark:bg-red-500 focus:ring-red-500 dark:focus:ring-red-500'
: ''
+11 -1
View File
@@ -12,6 +12,7 @@ import Spinner from '~/components/Spinner'
import TableList from '~/components/TableList'
import { getConfig, getContext, patchConfig } from '~/utils/config'
import { restartHeadscale } from '~/utils/docker'
import { getSession } from '~/utils/sessions'
import { useLiveData } from '~/utils/useLiveData'
import Domains from './domains'
@@ -45,9 +46,18 @@ export async function loader() {
}
export async function action({ request }: ActionFunctionArgs) {
const session = await getSession(request.headers.get('Cookie'))
if (!session.has('hsApiKey')) {
return json({ success: false }, {
status: 401
})
}
const context = await getContext()
if (!context.hasConfigWrite) {
return json({ success: false })
return json({ success: false }, {
status: 403
})
}
const data = await request.json() as Record<string, unknown>
+62 -25
View File
@@ -45,29 +45,9 @@ export async function action({ request }: ActionFunctionArgs) {
export default function Page() {
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'
}
)
}
})
const { Modal, open } = useModal()
return (
<>
@@ -143,17 +123,74 @@ export default function Page() {
)}
>
<Dropdown
className='left-1/4 w-min'
className='left-1/4'
width='w-48'
button={(
<EllipsisHorizontalIcon className='w-5 h-5'/>
)}
>
<Dropdown.Item className='text-red-700'>
<Dropdown.Item variant='static'>
<button
type='button' onClick={() => {
setActiveId(machine.id)
disabled
type='button'
className='text-left w-full opacity-50'
onClick={() => {
open()
}}
>
Edit machine name
</button>
</Dropdown.Item>
<Dropdown.Item variant='static'>
<button
disabled
type='button'
className='text-left w-full opacity-50'
onClick={() => {
open()
}}
>
Edit route settings
</button>
</Dropdown.Item>
<Dropdown.Item variant='static'>
<button
disabled
type='button'
className='text-left w-full opacity-50'
onClick={() => {
open()
}}
>
Edit ACL tags
</button>
</Dropdown.Item>
<Dropdown.Item>
<button
type='button'
className='text-left text-red-700 w-full'
onClick={() => {
open({
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: machine.id
},
{
method: 'DELETE',
encType: 'application/json'
}
)
}
})
}}
>
Remove
</button>
+2 -2
View File
@@ -69,9 +69,9 @@ export default function Layout() {
<p className='font-bold'>{data.user?.name}</p>
<p>{data.user?.email}</p>
</Dropdown.Item>
<Dropdown.Item className='text-red-700'>
<Dropdown.Item className='text-red-700 cursor-pointer'>
<Form method='POST' action='/logout'>
<button type='submit'>
<button type='submit' className='w-full'>
Logout
</button>
</Form>