feat: make the modal and dropdown more workable
This commit is contained in:
parent
f1347803a4
commit
bdb00b6cd7
@ -7,6 +7,7 @@ type Properties = {
|
|||||||
readonly button: ReactNode;
|
readonly button: ReactNode;
|
||||||
// eslint-disable-next-line unicorn/no-keyword-prefix
|
// eslint-disable-next-line unicorn/no-keyword-prefix
|
||||||
readonly className?: string;
|
readonly className?: string;
|
||||||
|
readonly width?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Dropdown(properties: Properties) {
|
function Dropdown(properties: Properties) {
|
||||||
@ -26,12 +27,13 @@ function Dropdown(properties: Properties) {
|
|||||||
leaveTo='transform opacity-0 scale-95'
|
leaveTo='transform opacity-0 scale-95'
|
||||||
>
|
>
|
||||||
<Menu.Items className={clsx(
|
<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',
|
'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',
|
'overflow-hidden z-50',
|
||||||
'border border-gray-200 dark:border-zinc-700',
|
'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}
|
{properties.children}
|
||||||
@ -66,7 +68,7 @@ function Item(properties: ItemProperties) {
|
|||||||
<div
|
<div
|
||||||
{...properties}
|
{...properties}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'px-4 py-2 w-full text-right',
|
'px-4 py-2 w-full',
|
||||||
'focus:outline-none focus:ring',
|
'focus:outline-none focus:ring',
|
||||||
'focus:ring-gray-300 dark:focus:ring-zinc-700',
|
'focus:ring-gray-300 dark:focus:ring-zinc-700',
|
||||||
properties.className,
|
properties.className,
|
||||||
|
|||||||
@ -14,36 +14,60 @@ type HookParameters = {
|
|||||||
|
|
||||||
// Optional because the button submits
|
// Optional because the button submits
|
||||||
onConfirm?: () => void | Promise<void>;
|
onConfirm?: () => void | Promise<void>;
|
||||||
|
onOpen?: () => void | Promise<void>;
|
||||||
|
onClose?: () => void | Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Overrides = Omit<HookParameters, 'onOpen' | 'onClose'>
|
||||||
|
|
||||||
type Properties = {
|
type Properties = {
|
||||||
readonly isOpen: boolean;
|
readonly isOpen: boolean;
|
||||||
readonly setIsOpen: (value: SetStateAction<boolean>) => void;
|
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 [isOpen, setIsOpen] = useState(false)
|
||||||
|
const [liveProperties, setLiveProperties] = useState(properties)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
Modal: (
|
Modal: (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
setIsOpen={setIsOpen}
|
setIsOpen={setIsOpen}
|
||||||
parameters={properties}
|
parameters={liveProperties}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
|
||||||
open: () => {
|
open: (overrides?: Overrides) => {
|
||||||
|
if (!overrides && !properties) {
|
||||||
|
throw new Error('No properties provided')
|
||||||
|
}
|
||||||
|
|
||||||
setIsOpen(true)
|
setIsOpen(true)
|
||||||
|
if (properties?.onOpen) {
|
||||||
|
void properties.onOpen()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (overrides) {
|
||||||
|
setLiveProperties(overrides)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
close: () => {
|
close: () => {
|
||||||
setIsOpen(false)
|
setIsOpen(false)
|
||||||
|
if (properties?.onClose) {
|
||||||
|
void properties.onClose()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function Modal({ parameters, isOpen, setIsOpen }: Properties) {
|
function Modal({ parameters, isOpen, setIsOpen }: Properties) {
|
||||||
|
if (!parameters) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Transition
|
<Transition
|
||||||
show={isOpen}
|
show={isOpen}
|
||||||
|
|||||||
@ -45,29 +45,9 @@ export async function action({ request }: ActionFunctionArgs) {
|
|||||||
export default function Page() {
|
export default function Page() {
|
||||||
useLiveData({ interval: 3000 })
|
useLiveData({ interval: 3000 })
|
||||||
const data = useLoaderData<typeof loader>()
|
const data = useLoaderData<typeof loader>()
|
||||||
const [activeId, setActiveId] = useState<string | undefined>(undefined)
|
|
||||||
const fetcher = useFetcher()
|
const fetcher = useFetcher()
|
||||||
|
|
||||||
const { Modal, open } = useModal({
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -143,17 +123,71 @@ export default function Page() {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
className='left-1/4 w-min'
|
className='left-1/4 cursor-pointer'
|
||||||
|
width='w-48'
|
||||||
button={(
|
button={(
|
||||||
<EllipsisHorizontalIcon className='w-5 h-5'/>
|
<EllipsisHorizontalIcon className='w-5 h-5'/>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Dropdown.Item className='text-red-700'>
|
<Dropdown.Item>
|
||||||
<button
|
<button
|
||||||
type='button' onClick={() => {
|
type='button'
|
||||||
setActiveId(machine.id)
|
className='text-left'
|
||||||
|
onClick={() => {
|
||||||
open()
|
open()
|
||||||
}}
|
}}
|
||||||
|
>
|
||||||
|
Edit machine name
|
||||||
|
</button>
|
||||||
|
</Dropdown.Item>
|
||||||
|
<Dropdown.Item>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='text-left'
|
||||||
|
onClick={() => {
|
||||||
|
open()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Edit route settings
|
||||||
|
</button>
|
||||||
|
</Dropdown.Item>
|
||||||
|
<Dropdown.Item>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='text-left'
|
||||||
|
onClick={() => {
|
||||||
|
open()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Edit ACL tags
|
||||||
|
</button>
|
||||||
|
</Dropdown.Item>
|
||||||
|
<Dropdown.Item>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='text-left text-red-700'
|
||||||
|
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
|
Remove
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user