feat: begin support for components and dark mode

This commit is contained in:
Aarnav Tale
2024-03-29 15:41:10 -04:00
parent abb957c573
commit b5658750a9
12 changed files with 195 additions and 89 deletions
+25
View File
@@ -0,0 +1,25 @@
import clsx from 'clsx'
import { type HTMLProps } from 'react'
type Properties = HTMLProps<HTMLButtonElement> & {
readonly isDestructive?: boolean;
readonly isDisabled?: boolean;
}
export default function Action(properties: Properties) {
return (
<button
{...properties}
type='button'
className={clsx(
properties.className,
properties.isDisabled && 'opacity-50 cursor-not-allowed',
properties.isDestructive
? 'text-red-700 dark:text-red-500'
: 'text-blue-700 dark:text-blue-400'
)}
>
{properties.children}
</button>
)
}
+18
View File
@@ -0,0 +1,18 @@
import clsx from 'clsx'
import { type HTMLProps } from 'react'
type Properties = HTMLProps<HTMLDivElement>
export default function Card(properties: Properties) {
return (
<div
{...properties}
className={clsx(
'p-4 md:p-6 border dark:border-zinc-700 rounded-lg',
properties.className
)}
>
{properties.children}
</div>
)
}
+9
View File
@@ -0,0 +1,9 @@
import { type ReactNode } from 'react'
export default function Code({ children }: { readonly children: ReactNode }) {
return (
<code className='bg-gray-100 dark:bg-zinc-700 p-0.5 rounded-md'>
{children}
</code>
)
}
+26
View File
@@ -0,0 +1,26 @@
import clsx from 'clsx'
import { type DetailedHTMLProps, type InputHTMLAttributes } from 'react'
type Properties = {
readonly isEmbedded?: boolean;
} & DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>
export default function Input(properties: Properties) {
return (
<input
{...properties}
className={clsx(
'block w-full dark:text-gray-300',
'border-gray-300 dark:border-zinc-700',
'focus:outline-none focus:ring',
'focus:ring-blue-500 dark:focus:ring-blue-300',
properties.isEmbedded ? 'bg-transparent' : 'dark:bg-zinc-800',
properties.isEmbedded ? 'p-0' : 'px-2.5 py-1.5',
properties.isEmbedded ? 'border-none' : 'border',
properties.isEmbedded ? 'focus:ring-0' : 'focus:ring-1',
properties.isEmbedded ? 'rounded-none' : 'rounded-lg',
properties.className
)}
/>
)
}
+37
View File
@@ -0,0 +1,37 @@
import clsx from 'clsx'
import { type HTMLProps } from 'react'
function TableList(properties: HTMLProps<HTMLDivElement>) {
return (
<div
{...properties}
className={clsx(
'border border-gray-300 rounded-lg overflow-clip',
'dark:border-zinc-700 dark:text-gray-300',
// 'dark:bg-zinc-800',
properties.className
)}
>
{properties.children}
</div>
)
}
function Item(properties: HTMLProps<HTMLDivElement>) {
return (
<div
{...properties}
className={clsx(
'flex items-center justify-between px-3 py-2',
'border-b border-gray-200 last:border-b-0',
'dark:border-zinc-800',
properties.className
)}
>
{properties.children}
</div>
)
}
export default Object.assign(TableList, { Item })
+4 -3
View File
@@ -11,6 +11,7 @@ import {
} from '@remix-run/react' } from '@remix-run/react'
import clsx from 'clsx' import clsx from 'clsx'
import Code from '~/components/Code'
import Toaster from '~/components/Toaster' import Toaster from '~/components/Toaster'
import stylesheet from '~/tailwind.css?url' import stylesheet from '~/tailwind.css?url'
import { getContext } from '~/utils/config' import { getContext } from '~/utils/config'
@@ -56,7 +57,7 @@ export function Layout({ children }: { readonly children: React.ReactNode }) {
<Meta/> <Meta/>
<Links/> <Links/>
</head> </head>
<body className='overscroll-none'> <body className='overscroll-none dark:bg-zinc-900 dark:text-white'>
{children} {children}
<Toaster/> <Toaster/>
<ScrollRestoration/> <ScrollRestoration/>
@@ -87,9 +88,9 @@ export function ErrorBoundary() {
<> <>
<ExclamationTriangleIcon className='text-red-500 w-14 h-14'/> <ExclamationTriangleIcon className='text-red-500 w-14 h-14'/>
<h1 className='text-2xl font-bold'>Error</h1> <h1 className='text-2xl font-bold'>Error</h1>
<code className='bg-gray-100 p-1 rounded-md'> <Code>
{message} {message}
</code> </Code>
<p className='opacity-50 text-sm mt-4'> <p className='opacity-50 text-sm mt-4'>
If you are the administrator of this site, please check your logs for information. If you are the administrator of this site, please check your logs for information.
</p> </p>
+23 -34
View File
@@ -15,11 +15,15 @@ import {
verticalListSortingStrategy verticalListSortingStrategy
} from '@dnd-kit/sortable' } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities' import { CSS } from '@dnd-kit/utilities'
import { Bars3Icon } from '@heroicons/react/24/outline' import { Bars3Icon, LockClosedIcon } from '@heroicons/react/24/outline'
import { useFetcher } from '@remix-run/react' import { useFetcher } from '@remix-run/react'
import clsx from 'clsx' import clsx from 'clsx'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import Action from '~/components/Action'
import Input from '~/components/Input'
import TableList from '~/components/TableList'
type Properties = { type Properties = {
readonly baseDomain?: string; readonly baseDomain?: string;
readonly searchDomains: string[]; readonly searchDomains: string[];
@@ -72,17 +76,12 @@ export default function Domains({ baseDomain, searchDomains }: Properties) {
} }
}} }}
> >
<div className='border border-gray-200 rounded-lg bg-gray-50 overflow-clip'> <TableList>
{baseDomain ? ( {baseDomain ? (
<div <TableList.Item key='magic-dns-sd'>
key='magic-dns-sd'
className={clsx(
'flex items-center justify-between px-3 py-2',
'border-b border-gray-200 last:border-b-0'
)}
>
<p className='font-mono text-sm'>{baseDomain}</p> <p className='font-mono text-sm'>{baseDomain}</p>
</div> <LockClosedIcon className='h-4 w-4'/>
</TableList.Item>
) : undefined} ) : undefined}
<SortableContext <SortableContext
items={localDomains} items={localDomains}
@@ -101,30 +100,20 @@ export default function Domains({ baseDomain, searchDomains }: Properties) {
/> : undefined} /> : undefined}
</DragOverlay> </DragOverlay>
</SortableContext> </SortableContext>
<div <TableList.Item key='add-sd'>
key='add-sd' <Input
className={clsx( isEmbedded
'flex items-center justify-between px-3 py-2',
'border-b border-gray-200 last:border-b-0',
'bg-white dark:bg-gray-800'
)}
>
<input
type='text' type='text'
className='w-full focus:ring-none focus:outline-none font-mono text-sm' className='font-mono text-sm'
placeholder='Search Domain' placeholder='Search Domain'
value={newDomain} value={newDomain}
onChange={event => { onChange={event => {
setNewDomain(event.target.value) setNewDomain(event.target.value)
}} }}
/> />
<button <Action
type='button' className='text-sm'
className={clsx( isDisabled={newDomain.length === 0}
'text-sm text-blue-700',
newDomain.length === 0 ? 'opacity-50 cursor-not-allowed' : ''
)}
disabled={newDomain.length === 0}
onClick={() => { onClick={() => {
fetcher.submit({ fetcher.submit({
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
@@ -138,9 +127,9 @@ export default function Domains({ baseDomain, searchDomains }: Properties) {
}} }}
> >
Add Add
</button> </Action>
</div> </TableList.Item>
</div> </TableList>
</DndContext> </DndContext>
</div> </div>
) )
@@ -188,9 +177,9 @@ function Domain({ domain, id, localDomains, isDrag }: DomainProperties) {
{domain} {domain}
</p> </p>
{isDrag ? undefined : ( {isDrag ? undefined : (
<button <Action
type='button' isDestructive
className='text-sm text-red-700' className='text-sm'
onClick={() => { onClick={() => {
fetcher.submit({ fetcher.submit({
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
@@ -202,7 +191,7 @@ function Domain({ domain, id, localDomains, isDrag }: DomainProperties) {
}} }}
> >
Remove Remove
</button> </Action>
)} )}
</div> </div>
) )
+11 -10
View File
@@ -2,9 +2,11 @@
/* eslint-disable unicorn/no-keyword-prefix */ /* eslint-disable unicorn/no-keyword-prefix */
import { Dialog } from '@headlessui/react' import { Dialog } from '@headlessui/react'
import { useFetcher } from '@remix-run/react' import { useFetcher } from '@remix-run/react'
import clsx from 'clsx'
import { useState } from 'react' import { useState } from 'react'
import Code from '~/components/Code'
import Input from '~/components/Input'
type Properties = { type Properties = {
readonly name: string; readonly name: string;
} }
@@ -21,18 +23,17 @@ export default function Modal({ name }: Properties) {
This is the base domain name of your Tailnet. This is the base domain name of your Tailnet.
Devices are accessible at Devices are accessible at
{' '} {' '}
<code className='bg-gray-100 dark:bg-zinc-700 p-0.5 rounded-md'> <Code>
[device].[user].{name} [device].[user].{name}
</code> </Code>
{' '} {' '}
when Magic DNS is enabled. when Magic DNS is enabled.
</p> </p>
<input <Input
readOnly readOnly
className={clsx( className='font-mono text-sm my-4'
'my-4 px-3 py-2 border rounded-lg focus:ring-none w-2/3 font-mono text-sm', // 'my-4 px-3 py-2 border rounded-lg focus:ring-none w-2/3 font-mono text-sm',
'dark:bg-zinc-800 dark:text-white dark:border-zinc-700' // 'dark:bg-zinc-800 dark:text-white dark:border-zinc-700'
)}
type='text' type='text'
value={name} value={name}
onFocus={event => { onFocus={event => {
@@ -65,9 +66,9 @@ export default function Modal({ name }: Properties) {
of unexpected behavior and may break existing devices of unexpected behavior and may break existing devices
in your tailnet. in your tailnet.
</Dialog.Description> </Dialog.Description>
<input <Input
type='text' type='text'
className='border rounded-lg p-2 w-full mt-4 dark:bg-zinc-700 dark:text-white dark:border-zinc-700' className='font-mono mt-4'
value={newName} value={newName}
onChange={event => { onChange={event => {
setNewName(event.target.value) setNewName(event.target.value)
+33 -36
View File
@@ -4,6 +4,10 @@ import { json, useFetcher, useLoaderData } from '@remix-run/react'
import clsx from 'clsx' import clsx from 'clsx'
import { useState } from 'react' import { useState } from 'react'
import Action from '~/components/Action'
import Code from '~/components/Code'
import Input from '~/components/Input'
import TableList from '~/components/TableList'
import { getConfig, patchConfig } from '~/utils/config' import { getConfig, patchConfig } from '~/utils/config'
import { restartHeadscale } from '~/utils/docker' import { restartHeadscale } from '~/utils/docker'
@@ -85,49 +89,42 @@ export default function Page() {
</Switch> </Switch>
</div> </div>
</div> </div>
<div className='border border-gray-200 rounded-lg bg-gray-50 overflow-clip'> <TableList>
{data.nameservers.map((ns, index) => ( {data.nameservers.map((ns, index) => (
<div // eslint-disable-next-line react/no-array-index-key
// eslint-disable-next-line react/no-array-index-key <TableList.Item key={index}>
key={index}
className={clsx(
'flex items-center justify-between px-3 py-2',
'border-b border-gray-200 last:border-b-0'
)}
>
<p className='font-mono text-sm'>{ns}</p> <p className='font-mono text-sm'>{ns}</p>
<button <Action
type='button' isDestructive
className='text-sm text-red-700' className='text-sm'
onClick={() => {
fetcher.submit({
// eslint-disable-next-line @typescript-eslint/naming-convention
'dns_config.nameservers': data.nameservers.filter((_, index_) => index_ !== index)
}, {
method: 'PATCH',
encType: 'application/json'
})
}}
> >
Remove Remove
</button> </Action>
</div> </TableList.Item>
))} ))}
<div <TableList.Item>
key='add-ns' <Input
className={clsx( isEmbedded
'flex items-center justify-between px-3 py-2',
'border-b border-gray-200 last:border-b-0',
'bg-white dark:bg-gray-800'
)}
>
<input
type='text' type='text'
className='w-full focus:ring-none focus:outline-none font-mono text-sm' className='font-mono text-sm'
placeholder='Nameserver' placeholder='Nameserver'
value={ns} value={ns}
onChange={event => { onChange={event => {
setNs(event.target.value) setNs(event.target.value)
}} }}
/> />
<button <Action
type='button' className='text-sm'
className={clsx( isDisabled={ns.length === 0}
'text-sm text-blue-700',
ns.length === 0 ? 'opacity-50 cursor-not-allowed' : ''
)}
disabled={ns.length === 0}
onClick={() => { onClick={() => {
fetcher.submit({ fetcher.submit({
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
@@ -141,9 +138,9 @@ export default function Page() {
}} }}
> >
Add Add
</button> </Action>
</div> </TableList.Item>
</div> </TableList>
{/* TODO: Split DNS and Custom A Records */} {/* TODO: Split DNS and Custom A Records */}
</div> </div>
</div> </div>
@@ -159,9 +156,9 @@ export default function Page() {
Automaticall register domain names for each device Automaticall register domain names for each device
on the tailnet. Devices will be accessible at on the tailnet. Devices will be accessible at
{' '} {' '}
<code className='bg-gray-100 p-1 rounded-md'> <Code>
[device].[user].{data.baseDomain} [device].[user].{data.baseDomain}
</code> </Code>
{' '} {' '}
when Magic DNS is enabled. when Magic DNS is enabled.
</p> </p>
+3 -2
View File
@@ -2,6 +2,7 @@ import { type LoaderFunctionArgs } from '@remix-run/node'
import { Link, useLoaderData } from '@remix-run/react' import { Link, useLoaderData } from '@remix-run/react'
import Attribute from '~/components/Attribute' import Attribute from '~/components/Attribute'
import Card from '~/components/Card'
import StatusCircle from '~/components/StatusCircle' import StatusCircle from '~/components/StatusCircle'
import { type Machine } from '~/types' import { type Machine } from '~/types'
import { pull } from '~/utils/headscale' import { pull } from '~/utils/headscale'
@@ -41,7 +42,7 @@ export default function Page() {
</h1> </h1>
<StatusCircle isOnline={data.online} className='w-4 h-4'/> <StatusCircle isOnline={data.online} className='w-4 h-4'/>
</span> </span>
<div className='p-4 md:p-6 border dark:border-zinc-700 rounded-lg'> <Card>
<Attribute name='Creator' value={data.user.name}/> <Attribute name='Creator' value={data.user.name}/>
<Attribute name='Node ID' value={data.id}/> <Attribute name='Node ID' value={data.id}/>
<Attribute name='Node Name' value={data.givenName}/> <Attribute name='Node Name' value={data.givenName}/>
@@ -68,7 +69,7 @@ export default function Page() {
name='Domain' name='Domain'
value={`${data.givenName}.${data.user.name}.ts.net`} value={`${data.givenName}.${data.user.name}.ts.net`}
/> />
</div> </Card>
</div> </div>
) )
} }
+3 -2
View File
@@ -5,6 +5,7 @@ import { useLoaderData } from '@remix-run/react'
import { toast } from 'react-hot-toast/headless' import { toast } from 'react-hot-toast/headless'
import Attribute from '~/components/Attribute' import Attribute from '~/components/Attribute'
import Card from '~/components/Card'
import StatusCircle from '~/components/StatusCircle' import StatusCircle from '~/components/StatusCircle'
import { type Machine, type User } from '~/types' import { type Machine, type User } from '~/types'
import { pull } from '~/utils/headscale' import { pull } from '~/utils/headscale'
@@ -44,7 +45,7 @@ export default function Page() {
return ( return (
<div className='grid grid-cols-2 gap-4 auto-rows-min'> <div className='grid grid-cols-2 gap-4 auto-rows-min'>
{data.map(user => ( {data.map(user => (
<div key={user.id} className='border rounded-lg p-4'> <Card key={user.id}>
<div className='flex items-center gap-4'> <div className='flex items-center gap-4'>
<UserIcon className='w-6 h-6'/> <UserIcon className='w-6 h-6'/>
<span className='text-lg font-mono'> <span className='text-lg font-mono'>
@@ -59,7 +60,7 @@ export default function Page() {
</div> </div>
))} ))}
</div> </div>
</div> </Card>
))} ))}
</div> </div>
) )
+3 -2
View File
@@ -2,6 +2,7 @@ import { type ActionFunctionArgs, json, type LoaderFunctionArgs, redirect } from
import { Form, Link, useActionData, useLoaderData } from '@remix-run/react' import { Form, Link, useActionData, useLoaderData } from '@remix-run/react'
import { useMemo } from 'react' import { useMemo } from 'react'
import Code from '~/components/Code'
import { pull } from '~/utils/headscale' import { pull } from '~/utils/headscale'
import { startOidc } from '~/utils/oidc' import { startOidc } from '~/utils/oidc'
import { commitSession, getSession } from '~/utils/sessions' import { commitSession, getSession } from '~/utils/sessions'
@@ -82,9 +83,9 @@ export default function Page() {
Enter an API key to authenticate with Headplane. You can generate Enter an API key to authenticate with Headplane. You can generate
one by running one by running
{' '} {' '}
<code className='bg-gray-100 p-1 rounded-md'> <Code>
headscale apikeys create headscale apikeys create
</code> </Code>
{' '} {' '}
in your terminal. in your terminal.
</p> </p>