feat: add loading states

This commit is contained in:
Aarnav Tale
2024-03-30 04:12:37 -04:00
parent cc1427882f
commit 935e015be9
5 changed files with 86 additions and 42 deletions
+23
View File
@@ -0,0 +1,23 @@
import clsx from 'clsx'
type Properties = {
// eslint-disable-next-line unicorn/no-keyword-prefix
className?: string;
}
export default function Spinner(properties: Properties) {
return (
<div className={clsx('mr-1.5 inline-block align-middle mb-0.5', properties.className)}>
<div
className={clsx(
'animate-spin rounded-full w-full h-full',
'border-2 border-current border-t-transparent',
properties.className
)}
role='status'
>
<span className='sr-only'>Loading...</span>
</div>
</div>
)
}
+30 -23
View File
@@ -16,12 +16,13 @@ import {
} from '@dnd-kit/sortable' } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities' import { CSS } from '@dnd-kit/utilities'
import { Bars3Icon, LockClosedIcon } from '@heroicons/react/24/outline' import { Bars3Icon, LockClosedIcon } from '@heroicons/react/24/outline'
import { useFetcher } from '@remix-run/react' import { type FetcherWithComponents, useFetcher } from '@remix-run/react'
import clsx from 'clsx' import clsx from 'clsx'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import Button from '~/components/Button' import Button from '~/components/Button'
import Input from '~/components/Input' import Input from '~/components/Input'
import Spinner from '~/components/Spinner'
import TableList from '~/components/TableList' import TableList from '~/components/TableList'
type Properties = { type Properties = {
@@ -97,6 +98,7 @@ export default function Domains({ baseDomain, searchDomains, disabled }: Propert
id={index + 1} id={index + 1}
localDomains={localDomains} localDomains={localDomains}
disabled={disabled} disabled={disabled}
fetcher={fetcher}
/> />
))} ))}
<DragOverlay adjustScale> <DragOverlay adjustScale>
@@ -106,6 +108,7 @@ export default function Domains({ baseDomain, searchDomains, disabled }: Propert
localDomains={localDomains} localDomains={localDomains}
id={activeId as number - 1} id={activeId as number - 1}
disabled={disabled} disabled={disabled}
fetcher={fetcher}
/> : undefined} /> : undefined}
</DragOverlay> </DragOverlay>
</SortableContext> </SortableContext>
@@ -121,23 +124,27 @@ export default function Domains({ baseDomain, searchDomains, disabled }: Propert
setNewDomain(event.target.value) setNewDomain(event.target.value)
}} }}
/> />
<Button {fetcher.state === 'idle' ? (
className='text-sm' <Button
disabled={newDomain.length === 0} className='text-sm'
onClick={() => { disabled={newDomain.length === 0}
fetcher.submit({ onClick={() => {
fetcher.submit({
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
'dns_config.domains': [...localDomains, newDomain] 'dns_config.domains': [...localDomains, newDomain]
}, { }, {
method: 'PATCH', method: 'PATCH',
encType: 'application/json' encType: 'application/json'
}) })
setNewDomain('') setNewDomain('')
}} }}
> >
Add Add
</Button> </Button>
) : (
<Spinner className='w-3 h-3 mr-0'/>
)}
</TableList.Item> </TableList.Item>
)} )}
</TableList> </TableList>
@@ -153,11 +160,10 @@ type DomainProperties = {
readonly localDomains: string[]; readonly localDomains: string[];
// eslint-disable-next-line react/boolean-prop-naming // eslint-disable-next-line react/boolean-prop-naming
readonly disabled?: boolean; readonly disabled?: boolean;
readonly fetcher: FetcherWithComponents<unknown>;
} }
function Domain({ domain, id, localDomains, isDrag, disabled }: DomainProperties) { function Domain({ domain, id, localDomains, isDrag, disabled, fetcher }: DomainProperties) {
const fetcher = useFetcher()
const { const {
attributes, attributes,
listeners, listeners,
@@ -167,14 +173,15 @@ function Domain({ domain, id, localDomains, isDrag, disabled }: DomainProperties
isDragging isDragging
} = useSortable({ id }) } = useSortable({ id })
// TODO: Figure out why TableList.Item breaks dndkit
return ( return (
<div <div
ref={setNodeRef} ref={setNodeRef}
className={clsx( className={clsx(
'flex items-center justify-between px-3 py-2', 'flex items-center justify-between px-3 py-2',
'border-b border-gray-200 last:border-b-0', 'border-b border-gray-200 last:border-b-0 dark:border-zinc-800',
isDragging ? 'text-gray-400' : 'bg-gray-50', isDragging ? 'text-gray-400' : '',
isDrag ? 'outline outline-1 outline-gray-500' : undefined isDrag ? 'outline outline-1 outline-gray-500 bg-gray-200 dark:bg-zinc-800' : ''
)} )}
style={{ style={{
transform: CSS.Transform.toString(transform), transform: CSS.Transform.toString(transform),
@@ -198,7 +205,7 @@ function Domain({ domain, id, localDomains, isDrag, disabled }: DomainProperties
disabled={disabled} disabled={disabled}
onClick={() => { onClick={() => {
fetcher.submit({ fetcher.submit({
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
'dns_config.domains': localDomains.filter((_, index) => index !== id - 1) 'dns_config.domains': localDomains.filter((_, index) => index !== id - 1)
}, { }, {
method: 'PATCH', method: 'PATCH',
+4
View File
@@ -4,6 +4,7 @@ import clsx from 'clsx'
import { useState } from 'react' import { useState } from 'react'
import Button from '~/components/Button' import Button from '~/components/Button'
import Spinner from '~/components/Spinner'
type Properties = { type Properties = {
readonly isEnabled: boolean; readonly isEnabled: boolean;
@@ -25,6 +26,9 @@ export default function Modal({ isEnabled, disabled }: Properties) {
setIsOpen(true) setIsOpen(true)
}} }}
> >
{fetcher.state === 'idle' ? undefined : (
<Spinner className='w-3 h-3'/>
)}
{isEnabled ? 'Disable' : 'Enable'} Magic DNS {isEnabled ? 'Disable' : 'Enable'} Magic DNS
</Button> </Button>
<Dialog <Dialog
+4 -1
View File
@@ -7,6 +7,7 @@ import { useState } from 'react'
import Button from '~/components/Button' import Button from '~/components/Button'
import Code from '~/components/Code' import Code from '~/components/Code'
import Input from '~/components/Input' import Input from '~/components/Input'
import Spinner from '~/components/Spinner'
type Properties = { type Properties = {
readonly name: string; readonly name: string;
@@ -49,6 +50,9 @@ export default function Modal({ name, disabled }: Properties) {
setIsOpen(true) setIsOpen(true)
}} }}
> >
{fetcher.state === 'idle' ? undefined : (
<Spinner className='w-3 h-3'/>
)}
Rename Tailnet... Rename Tailnet...
</Button> </Button>
<Dialog <Dialog
@@ -88,7 +92,6 @@ export default function Modal({ name, disabled }: Properties) {
}) })
setIsOpen(false) setIsOpen(false)
setNewName(name)
}} }}
> >
Rename Rename
+25 -18
View File
@@ -8,6 +8,7 @@ import Button from '~/components/Button'
import Code from '~/components/Code' import Code from '~/components/Code'
import Input from '~/components/Input' import Input from '~/components/Input'
import Notice from '~/components/Notice' import Notice from '~/components/Notice'
import Spinner from '~/components/Spinner'
import TableList from '~/components/TableList' import TableList from '~/components/TableList'
import { getConfig, getContext, patchConfig } from '~/utils/config' import { getConfig, getContext, patchConfig } from '~/utils/config'
import { restartHeadscale } from '~/utils/docker' import { restartHeadscale } from '~/utils/docker'
@@ -80,12 +81,14 @@ export default function Page() {
Global Nameservers Global Nameservers
</h2> </h2>
<div className='flex gap-2 items-center'> <div className='flex gap-2 items-center'>
<span className='text-sm opacity-50'>Override local DNS</span> <span className='text-sm opacity-50'>
Override local DNS
</span>
<Switch <Switch
checked={localOverride} checked={localOverride}
disabled={!data.hasConfigWrite} disabled={!data.hasConfigWrite}
className={clsx( className={clsx(
localOverride ? 'bg-gray-800' : 'bg-gray-200', localOverride ? 'bg-gray-800 dark:bg-gray-600' : 'bg-gray-200 dark:bg-gray-400',
'relative inline-flex h-4 w-9 items-center rounded-full' 'relative inline-flex h-4 w-9 items-center rounded-full'
)} )}
onChange={() => { onChange={() => {
@@ -145,23 +148,27 @@ export default function Page() {
setNs(event.target.value) setNs(event.target.value)
}} }}
/> />
<Button {fetcher.state === 'idle' ? (
className='text-sm' <Button
disabled={ns.length === 0} className='text-sm'
onClick={() => { disabled={ns.length === 0}
fetcher.submit({ onClick={() => {
// eslint-disable-next-line @typescript-eslint/naming-convention fetcher.submit({
'dns_config.nameservers': [...data.nameservers, ns] // eslint-disable-next-line @typescript-eslint/naming-convention
}, { 'dns_config.nameservers': [...data.nameservers, ns]
method: 'PATCH', }, {
encType: 'application/json' method: 'PATCH',
}) encType: 'application/json'
})
setNs('') setNs('')
}} }}
> >
Add Add
</Button> </Button>
) : (
<Spinner className='w-3 h-3 mr-0'/>
)}
</TableList.Item> </TableList.Item>
) : undefined} ) : undefined}
</TableList> </TableList>