mirror of
https://github.com/tale/headplane.git
synced 2026-07-27 00:08:14 +00:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9f5ac6a9ef | |||
| 7f55ad826c | |||
| 2cc65e8783 | |||
| 5ff09e44d9 | |||
| c6cdcf35eb |
@@ -1,3 +1,9 @@
|
||||
### 0.1.3 (May 4, 2024)
|
||||
|
||||
- Switched to a better icon set for the UI.
|
||||
- Support stable scrollbar gutter if supported by the browser.
|
||||
- Cleaned up the header which fixed a bug that could crash the entire application on fetch errors.
|
||||
|
||||
### 0.1.2 (May 1, 2024)
|
||||
|
||||
- Added support for renaming, expiring, removing, and managing the routes of a machine.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ClipboardIcon } from '@heroicons/react/24/outline'
|
||||
import { CopyIcon } from '@primer/octicons-react'
|
||||
|
||||
import { toast } from './Toaster'
|
||||
|
||||
@@ -28,7 +28,7 @@ export default function Attribute({ name, value, isCopyable }: Properties) {
|
||||
<dd className='min-w-0 truncate px-2 py-1'>
|
||||
{value}
|
||||
</dd>
|
||||
<ClipboardIcon className='text-gray-600 dark:text-gray-200 pr-2 w-max h-4'/>
|
||||
<CopyIcon className='text-gray-600 dark:text-gray-200 pr-2 w-max h-4'/>
|
||||
</button>
|
||||
) : (
|
||||
<dd className='min-w-0 truncate px-2 py-1'>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'
|
||||
import { AlertIcon } from '@primer/octicons-react'
|
||||
import { isRouteErrorResponse, useRouteError } from '@remix-run/react'
|
||||
import { useState } from 'react'
|
||||
|
||||
@@ -23,7 +23,7 @@ export function ErrorPopup({ type = 'full' }: Properties) {
|
||||
<Dialog>
|
||||
<Dialog.Panel
|
||||
className={cn(
|
||||
type === 'embedded' ? 'pointer-events-none bg-transparent dark:bg-transparent' : '',
|
||||
type === 'embedded' ? 'pointer-events-none bg-transparent dark:bg-transparent' : ''
|
||||
)}
|
||||
control={open}
|
||||
>
|
||||
@@ -33,7 +33,7 @@ export function ErrorPopup({ type = 'full' }: Properties) {
|
||||
<Dialog.Title className='text-3xl mb-0'>
|
||||
{routing ? error.status : 'Error'}
|
||||
</Dialog.Title>
|
||||
<ExclamationTriangleIcon className='w-12 h-12 text-red-500'/>
|
||||
<AlertIcon className='w-12 h-12 text-red-500'/>
|
||||
</div>
|
||||
<Dialog.Text className='mt-4 text-lg'>
|
||||
{routing ? (
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import { GearIcon, GlobeIcon, LockIcon, PaperAirplaneIcon, PeopleIcon, PersonIcon, ServerIcon } from '@primer/octicons-react'
|
||||
import { Form } from '@remix-run/react'
|
||||
|
||||
import { cn } from '~/utils/cn'
|
||||
import { type Context } from '~/utils/config'
|
||||
import { type SessionData } from '~/utils/sessions'
|
||||
|
||||
import Menu from './Menu'
|
||||
import TabLink from './TabLink'
|
||||
|
||||
type Properties = {
|
||||
readonly data?: Context & { user?: SessionData['user'] };
|
||||
}
|
||||
|
||||
type LinkProperties = {
|
||||
readonly href: string;
|
||||
readonly text: string;
|
||||
readonly isMenu?: boolean;
|
||||
}
|
||||
|
||||
function Link({ href, text, isMenu }: LinkProperties) {
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
className={cn(
|
||||
!isMenu && 'text-ui-300 hover:text-ui-50 hover:underline hidden sm:block'
|
||||
)}
|
||||
>
|
||||
{text}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Header({ data }: Properties) {
|
||||
return (
|
||||
<header className='bg-main-700 dark:bg-main-800 text-ui-50'>
|
||||
<div className='container flex items-center justify-between py-4'>
|
||||
<div className='flex items-center gap-x-2'>
|
||||
<PaperAirplaneIcon className='w-6 h-6'/>
|
||||
<h1 className='text-2xl'>Headplane</h1>
|
||||
</div>
|
||||
<div className='flex items-center gap-x-4'>
|
||||
<Link href='https://tailscale.com/download' text='Download'/>
|
||||
<Link href='https://github.com/tale/headplane' text='GitHub'/>
|
||||
<Link href='https://github.com/juanfont/headscale' text='Headscale'/>
|
||||
{data?.user ? (
|
||||
<Menu>
|
||||
<Menu.Button className={cn(
|
||||
'rounded-full h-9 w-9',
|
||||
'border border-main-600 dark:border-main-700',
|
||||
'hover:bg-main-600 dark:hover:bg-main-700'
|
||||
)}
|
||||
>
|
||||
<PersonIcon className='h-5 w-5 mt-0.5'/>
|
||||
</Menu.Button>
|
||||
<Menu.Items>
|
||||
<Menu.Item className='text-right'>
|
||||
<p className='font-bold'>{data.user.name}</p>
|
||||
<p>{data.user.email}</p>
|
||||
</Menu.Item>
|
||||
<Menu.Item className='text-right sm:hidden'>
|
||||
<Link
|
||||
isMenu
|
||||
href='https://tailscale.com/download'
|
||||
text='Download'
|
||||
/>
|
||||
</Menu.Item>
|
||||
<Menu.Item className='text-right sm:hidden'>
|
||||
<Link
|
||||
isMenu
|
||||
href='https://github.com/tale/headplane'
|
||||
text='GitHub'
|
||||
/>
|
||||
</Menu.Item>
|
||||
<Menu.Item className='text-right sm:hidden'>
|
||||
<Link
|
||||
isMenu
|
||||
href='https://github.com/juanfont/headscale'
|
||||
text='Headscale'
|
||||
/>
|
||||
</Menu.Item>
|
||||
<Menu.Item className='text-red-500 dark:text-red-400'>
|
||||
<Form method='POST' action='/logout'>
|
||||
<button type='submit' className='w-full text-right'>
|
||||
Logout
|
||||
</button>
|
||||
</Form>
|
||||
</Menu.Item>
|
||||
</Menu.Items>
|
||||
</Menu>
|
||||
) : undefined}
|
||||
</div>
|
||||
</div>
|
||||
<nav className='container flex items-center gap-x-4 overflow-x-auto'>
|
||||
<TabLink to='/machines' name='Machines' icon={<ServerIcon className='w-4 h-4'/>}/>
|
||||
<TabLink to='/users' name='Users' icon={<PeopleIcon className='w-4 h-4'/>}/>
|
||||
{data?.hasAcl ? <TabLink to='/acls' name='Access Control' icon={<LockIcon className='w-4 h-4'/>}/> : undefined}
|
||||
{data?.hasConfig ? (
|
||||
<>
|
||||
<TabLink to='/dns' name='DNS' icon={<GlobeIcon className='w-4 h-4'/>}/>
|
||||
<TabLink to='/settings' name='Settings' icon={<GearIcon className='w-4 h-4'/>}/>
|
||||
</>
|
||||
) : undefined}
|
||||
</nav>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { LinkExternalIcon } from '@primer/octicons-react'
|
||||
|
||||
import { cn } from '~/utils/cn'
|
||||
|
||||
/* eslint-disable unicorn/no-keyword-prefix */
|
||||
type Properties = {
|
||||
readonly to: string;
|
||||
readonly name: string;
|
||||
readonly children: string;
|
||||
readonly className?: string;
|
||||
}
|
||||
|
||||
export default function Link({ to, name: alt, children, className }: Properties) {
|
||||
return (
|
||||
<a
|
||||
href={to}
|
||||
aria-label={alt}
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
className={cn(
|
||||
'inline-flex items-center gap-x-1',
|
||||
'text-blue-500 hover:text-blue-700',
|
||||
'dark:text-blue-400 dark:hover:text-blue-300',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
<LinkExternalIcon className='h-3 w-3'/>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { InformationCircleIcon } from '@heroicons/react/24/outline'
|
||||
import { InfoIcon } from '@primer/octicons-react'
|
||||
import clsx from 'clsx'
|
||||
import { type ReactNode } from 'react'
|
||||
|
||||
@@ -9,7 +9,7 @@ export default function Notice({ children }: { readonly children: ReactNode }) {
|
||||
'bg-slate-400 dark:bg-slate-700'
|
||||
)}
|
||||
>
|
||||
<InformationCircleIcon className='h-6 w-6 text-white'/>
|
||||
<InfoIcon className='h-6 w-6 text-white'/>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -13,8 +13,9 @@ export default function TabLink({ name, to, icon }: Properties) {
|
||||
<NavLink
|
||||
to={to}
|
||||
className={({ isActive, isPending }) => clsx(
|
||||
'flex items-center gap-x-2 p-2 border-b-2 text-md',
|
||||
isActive ? 'border-white' : 'border-transparent'
|
||||
'flex items-center gap-x-2 p-2 border-b-2 text-md text-nowrap',
|
||||
isActive ? 'border-white' : 'border-transparent',
|
||||
isPending && 'animate-pulse'
|
||||
)}
|
||||
>
|
||||
{icon} {name}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { XMarkIcon } from '@heroicons/react/24/outline'
|
||||
import { XIcon } from '@primer/octicons-react'
|
||||
import { type AriaToastProps, useToast, useToastRegion } from '@react-aria/toast'
|
||||
import { ToastQueue, type ToastState, useToastQueue } from '@react-stately/toast'
|
||||
import { type ReactNode, useRef } from 'react'
|
||||
@@ -35,7 +35,7 @@ function Toast({ state, ...properties }: ToastProperties) {
|
||||
'hover:bg-main-600 dark:hover:bg-main-700'
|
||||
)}
|
||||
>
|
||||
<XMarkIcon className='w-4 h-4'/>
|
||||
<XIcon className='w-4 h-4'/>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { BeakerIcon, CubeTransparentIcon, EyeIcon, PencilSquareIcon } from '@heroicons/react/24/outline'
|
||||
import { BeakerIcon, EyeIcon, IssueDraftIcon, PencilIcon } from '@primer/octicons-react'
|
||||
import { type ActionFunctionArgs, json } from '@remix-run/node'
|
||||
import { useLoaderData } from '@remix-run/react'
|
||||
import { useState } from 'react'
|
||||
import { Tab, TabList, TabPanel, Tabs } from 'react-aria-components'
|
||||
import { ClientOnly } from 'remix-utils/client-only'
|
||||
|
||||
import Link from '~/components/Link'
|
||||
import Notice from '~/components/Notice'
|
||||
import { cn } from '~/utils/cn'
|
||||
import { getAcl, getContext, patchAcl } from '~/utils/config'
|
||||
@@ -74,16 +75,24 @@ export default function Page() {
|
||||
|
||||
<p className='mb-4 max-w-prose'>
|
||||
The ACL file is used to define the access control rules for your network.
|
||||
You can find more information about the ACL file in the Tailscale documentation.
|
||||
You can find more information about the ACL file in the
|
||||
{' '}
|
||||
<a
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
href='https://tailscale.com/kb/1018/acls'
|
||||
className='text-blue-500 dark:text-blue-400 hover:underline'
|
||||
<Link
|
||||
to='https://tailscale.com/kb/1018/acls'
|
||||
name='Tailscale ACL documentation'
|
||||
>
|
||||
More information
|
||||
</a>
|
||||
Tailscale ACL guide
|
||||
</Link>
|
||||
{' '}
|
||||
and the
|
||||
{' '}
|
||||
<Link
|
||||
to='https://headscale.net/acls'
|
||||
name='Headscale ACL documentation'
|
||||
>
|
||||
Headscale docs
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
|
||||
<Tabs>
|
||||
@@ -102,7 +111,7 @@ export default function Page() {
|
||||
isSelected ? 'text-gray-900 dark:text-gray-100' : ''
|
||||
)}
|
||||
>
|
||||
<PencilSquareIcon className='w-5 h-5'/>
|
||||
<PencilIcon className='w-5 h-5'/>
|
||||
<p>Edit file</p>
|
||||
</Tab>
|
||||
<Tab
|
||||
@@ -152,7 +161,7 @@ export default function Page() {
|
||||
'p-16 flex flex-col items-center justify-center'
|
||||
)}
|
||||
>
|
||||
<CubeTransparentIcon className='w-24 h-24 text-gray-300 dark:text-gray-500'/>
|
||||
<IssueDraftIcon className='w-24 h-24 text-gray-300 dark:text-gray-500'/>
|
||||
<p className='w-1/2 text-center mt-4'>
|
||||
The Preview rules is very much still a work in progress.
|
||||
It is a bit complicated to implement right now but hopefully it will be available soon.
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
verticalListSortingStrategy
|
||||
} from '@dnd-kit/sortable'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
import { Bars3Icon, LockClosedIcon } from '@heroicons/react/24/outline'
|
||||
import { LockIcon, ThreeBarsIcon } from '@primer/octicons-react'
|
||||
import { type FetcherWithComponents, useFetcher } from '@remix-run/react'
|
||||
import clsx from 'clsx'
|
||||
import { useEffect, useState } from 'react'
|
||||
@@ -83,7 +83,7 @@ export default function Domains({ baseDomain, searchDomains, disabled }: Propert
|
||||
{baseDomain ? (
|
||||
<TableList.Item key='magic-dns-sd'>
|
||||
<p className='font-mono text-sm'>{baseDomain}</p>
|
||||
<LockClosedIcon className='h-4 w-4'/>
|
||||
<LockIcon className='h-4 w-4'/>
|
||||
</TableList.Item>
|
||||
) : undefined}
|
||||
<SortableContext
|
||||
@@ -190,7 +190,7 @@ function Domain({ domain, id, localDomains, isDrag, disabled, fetcher }: DomainP
|
||||
>
|
||||
<p className='font-mono text-sm flex items-center gap-4'>
|
||||
{disabled ? undefined : (
|
||||
<Bars3Icon
|
||||
<ThreeBarsIcon
|
||||
className='h-4 w-4 text-gray-400 focus:outline-none'
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable react/hook-use-state */
|
||||
import { ChevronDownIcon, ClipboardIcon, EllipsisHorizontalIcon } from '@heroicons/react/24/outline'
|
||||
import { ChevronDownIcon, CopyIcon, KebabHorizontalIcon } from '@primer/octicons-react'
|
||||
import { type FetcherWithComponents, Link } from '@remix-run/react'
|
||||
import { useState } from 'react'
|
||||
|
||||
@@ -96,7 +96,7 @@ export default function MachineRow({ machine, routes, fetcher, magic }: MachineP
|
||||
}}
|
||||
>
|
||||
{ip}
|
||||
<ClipboardIcon className='w-3 h-3'/>
|
||||
<CopyIcon className='w-3 h-3'/>
|
||||
</Menu.ItemButton>
|
||||
))}
|
||||
{magic ? (
|
||||
@@ -113,7 +113,7 @@ export default function MachineRow({ machine, routes, fetcher, magic }: MachineP
|
||||
}}
|
||||
>
|
||||
{machine.givenName}.{machine.user.name}.{magic}
|
||||
<ClipboardIcon className='w-3 h-3'/>
|
||||
<CopyIcon className='w-3 h-3'/>
|
||||
</Menu.ItemButton>
|
||||
) : undefined}
|
||||
</Menu.Items>
|
||||
@@ -173,7 +173,7 @@ export default function MachineRow({ machine, routes, fetcher, magic }: MachineP
|
||||
'group-hover:border-gray-200 dark:group-hover:border-zinc-700'
|
||||
)}
|
||||
>
|
||||
<EllipsisHorizontalIcon className='w-5'/>
|
||||
<KebabHorizontalIcon className='w-5'/>
|
||||
</Menu.Button>
|
||||
<Menu.Items>
|
||||
<Menu.ItemButton control={renameState}>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import { InformationCircleIcon } from '@heroicons/react/24/outline'
|
||||
import { InfoIcon } from '@primer/octicons-react'
|
||||
import { type ActionFunctionArgs, json, type LoaderFunctionArgs } from '@remix-run/node'
|
||||
import { useFetcher, useLoaderData } from '@remix-run/react'
|
||||
import { Button, Tooltip, TooltipTrigger } from 'react-aria-components'
|
||||
@@ -121,7 +121,7 @@ export default function Page() {
|
||||
{data.magic ? (
|
||||
<TooltipTrigger delay={0}>
|
||||
<Button>
|
||||
<InformationCircleIcon className='w-4 h-4 text-gray-400'/>
|
||||
<InfoIcon className='w-4 h-4'/>
|
||||
</Button>
|
||||
<Tooltip className={cn(
|
||||
'text-sm max-w-xs p-2 rounded-lg mb-2',
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { CubeTransparentIcon } from '@heroicons/react/24/outline'
|
||||
import { IssueDraftIcon } from '@primer/octicons-react'
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div className='w-96 mx-auto flex flex-col justify-center items-center text-center'>
|
||||
<CubeTransparentIcon className='w-32 h-32 text-gray-500'/>
|
||||
<div className='w-96 mx-auto flex flex-col justify-center items-center text-center my-8'>
|
||||
<IssueDraftIcon className='w-24 h-24 text-gray-300 dark:text-gray-500'/>
|
||||
<p className='text-lg mt-8'>
|
||||
The settings page is currently unavailable.
|
||||
It will be available in a future release.
|
||||
|
||||
+5
-71
@@ -1,10 +1,8 @@
|
||||
import { Cog8ToothIcon, CpuChipIcon, GlobeAltIcon, LockClosedIcon, ServerStackIcon, UserCircleIcon, UsersIcon } from '@heroicons/react/24/outline'
|
||||
import { type LoaderFunctionArgs, redirect } from '@remix-run/node'
|
||||
import { Form, Outlet, useLoaderData } from '@remix-run/react'
|
||||
import { Outlet, useLoaderData } from '@remix-run/react'
|
||||
|
||||
import { ErrorPopup } from '~/components/Error'
|
||||
import Menu from '~/components/Menu'
|
||||
import TabLink from '~/components/TabLink'
|
||||
import Header from '~/components/Header'
|
||||
import { getContext } from '~/utils/config'
|
||||
import { HeadscaleError, pull } from '~/utils/headscale'
|
||||
import { destroySession, getSession } from '~/utils/sessions'
|
||||
@@ -20,7 +18,6 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
await pull('v1/apikey', session.get('hsApiKey')!)
|
||||
} catch (error) {
|
||||
if (error instanceof HeadscaleError) {
|
||||
console.error(error)
|
||||
// Safest to just redirect to login if we can't pull
|
||||
return redirect('/login', {
|
||||
headers: {
|
||||
@@ -46,58 +43,9 @@ export default function Layout() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className='mb-6 bg-main-700 dark:bg-main-800 text-white'>
|
||||
<nav className='container mx-auto'>
|
||||
<div className='flex items-center justify-between mb-8 pt-4'>
|
||||
<div className='flex items-center gap-x-2'>
|
||||
<CpuChipIcon className='w-8 h-8'/>
|
||||
<h1 className='text-2xl'>Headplane</h1>
|
||||
</div>
|
||||
<div className='flex items-center gap-x-4'>
|
||||
<a href='https://tailscale.com/download' target='_blank' rel='noreferrer' className='text-gray-300 hover:text-white'>
|
||||
Download
|
||||
</a>
|
||||
<a href='https://github.com/tale/headplane' target='_blank' rel='noreferrer' className='text-gray-300 hover:text-white'>
|
||||
GitHub
|
||||
</a>
|
||||
<a href='https://github.com/juanfont/headscale' target='_blank' rel='noreferrer' className='text-gray-300 hover:text-white'>
|
||||
Headscale
|
||||
</a>
|
||||
<Menu>
|
||||
<Menu.Button>
|
||||
<UserCircleIcon className='w-8 h-8'/>
|
||||
</Menu.Button>
|
||||
<Menu.Items>
|
||||
<Menu.Item className='text-right'>
|
||||
<p className='font-bold'>{data.user?.name}</p>
|
||||
<p>{data.user?.email}</p>
|
||||
</Menu.Item>
|
||||
<Menu.Item className='text-red-500 dark:text-red-400'>
|
||||
<Form method='POST' action='/logout'>
|
||||
<button type='submit' className='w-full text-right'>
|
||||
Logout
|
||||
</button>
|
||||
</Form>
|
||||
</Menu.Item>
|
||||
</Menu.Items>
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center gap-x-4'>
|
||||
<TabLink to='/machines' name='Machines' icon={<ServerStackIcon className='w-5 h-5'/>}/>
|
||||
<TabLink to='/users' name='Users' icon={<UsersIcon className='w-5 h-5'/>}/>
|
||||
{data.hasAcl ? <TabLink to='/acls' name='Access Control' icon={<LockClosedIcon className='w-5 h-5'/>}/> : undefined}
|
||||
{data.hasConfig ? (
|
||||
<>
|
||||
<TabLink to='/dns' name='DNS' icon={<GlobeAltIcon className='w-5 h-5'/>}/>
|
||||
<TabLink to='/settings' name='Settings' icon={<Cog8ToothIcon className='w-5 h-5'/>}/>
|
||||
</>
|
||||
) : undefined}
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
<Header data={data}/>
|
||||
|
||||
<main className='container mx-auto overscroll-contain mb-24'>
|
||||
<main className='container mx-auto overscroll-contain mt-4 mb-24'>
|
||||
<Outlet/>
|
||||
</main>
|
||||
</>
|
||||
@@ -107,21 +55,7 @@ export default function Layout() {
|
||||
export function ErrorBoundary() {
|
||||
return (
|
||||
<>
|
||||
<header className='mb-16 bg-main-700 dark:bg-main-800 text-white'>
|
||||
<nav className='container mx-auto'>
|
||||
<div className='flex items-center gap-x-2 mb-8 pt-4'>
|
||||
<CpuChipIcon className='w-8 h-8'/>
|
||||
<h1 className='text-2xl'>Headplane</h1>
|
||||
</div>
|
||||
<div className='flex items-center gap-x-4'>
|
||||
<TabLink to='/machines' name='Machines' icon={<ServerStackIcon className='w-5 h-5'/>}/>
|
||||
<TabLink to='/users' name='Users' icon={<UsersIcon className='w-5 h-5'/>}/>
|
||||
<TabLink to='/acls' name='Access Control' icon={<LockClosedIcon className='w-5 h-5'/>}/>
|
||||
<TabLink to='/dns' name='DNS' icon={<GlobeAltIcon className='w-5 h-5'/>}/>
|
||||
<TabLink to='/settings' name='Settings' icon={<Cog8ToothIcon className='w-5 h-5'/>}/>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
<Header/>
|
||||
<ErrorPopup type='embedded'/>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
/* eslint-disable unicorn/filename-case */
|
||||
import { ClipboardIcon, UserIcon } from '@heroicons/react/24/outline'
|
||||
import { PersonIcon } from '@primer/octicons-react'
|
||||
import { type LoaderFunctionArgs } from '@remix-run/node'
|
||||
import { useLoaderData } from '@remix-run/react'
|
||||
import { toast } from 'react-hot-toast/headless'
|
||||
|
||||
import Attribute from '~/components/Attribute'
|
||||
import Card from '~/components/Card'
|
||||
import StatusCircle from '~/components/StatusCircle'
|
||||
import { type Machine, type User } from '~/types'
|
||||
import { type Machine } from '~/types'
|
||||
import { pull } from '~/utils/headscale'
|
||||
import { getSession } from '~/utils/sessions'
|
||||
import { useLiveData } from '~/utils/useLiveData'
|
||||
@@ -47,7 +46,7 @@ export default function Page() {
|
||||
{data.map(user => (
|
||||
<Card key={user.id}>
|
||||
<div className='flex items-center gap-4'>
|
||||
<UserIcon className='w-6 h-6'/>
|
||||
<PersonIcon className='w-6 h-6'/>
|
||||
<span className='text-lg font-mono'>
|
||||
{user.name}
|
||||
</span>
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@supports (scrollbar-gutter: stable) {
|
||||
html {
|
||||
scrollbar-gutter: stable
|
||||
}
|
||||
|
||||
html body {
|
||||
margin-right: 0!important;
|
||||
--removed-body-scroll-bar-size: 0 !important
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -196,7 +196,7 @@ export function registerConfigWatcher() {
|
||||
})
|
||||
}
|
||||
|
||||
type Context = {
|
||||
export type Context = {
|
||||
hasDockerSock: boolean;
|
||||
hasConfig: boolean;
|
||||
hasConfigWrite: boolean;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createCookieSessionStorage } from '@remix-run/node' // Or cloudflare/deno
|
||||
|
||||
type SessionData = {
|
||||
export type SessionData = {
|
||||
hsApiKey: string;
|
||||
authState: string;
|
||||
authNonce: string;
|
||||
|
||||
+24
-24
@@ -17,40 +17,40 @@
|
||||
"@dnd-kit/modifiers": "^7.0.0",
|
||||
"@dnd-kit/sortable": "^8.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@heroicons/react": "^2.1.3",
|
||||
"@react-aria/toast": "3.0.0-beta.10",
|
||||
"@primer/octicons-react": "^19.9.0",
|
||||
"@react-aria/toast": "3.0.0-beta.11",
|
||||
"@react-stately/toast": "3.0.0-beta.2",
|
||||
"@remix-run/node": "^2.8.1",
|
||||
"@remix-run/react": "^2.8.1",
|
||||
"@remix-run/serve": "^2.8.1",
|
||||
"@uiw/codemirror-theme-github": "^4.21.25",
|
||||
"@uiw/react-codemirror": "^4.21.25",
|
||||
"clsx": "^2.1.0",
|
||||
"isbot": "^4.1.0",
|
||||
"oauth4webapi": "^2.10.3",
|
||||
"react": "^18.2.0",
|
||||
"react-aria-components": "^1.1.1",
|
||||
"react-codemirror-merge": "^4.21.25",
|
||||
"react-dom": "^18.2.0",
|
||||
"@remix-run/node": "^2.9.1",
|
||||
"@remix-run/react": "^2.9.1",
|
||||
"@remix-run/serve": "^2.9.1",
|
||||
"@uiw/codemirror-theme-github": "^4.22.0",
|
||||
"@uiw/react-codemirror": "^4.22.0",
|
||||
"clsx": "^2.1.1",
|
||||
"isbot": "^5.1.6",
|
||||
"oauth4webapi": "^2.10.4",
|
||||
"react": "^18.3.1",
|
||||
"react-aria-components": "^1.2.0",
|
||||
"react-codemirror-merge": "^4.22.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"remix-utils": "^7.6.0",
|
||||
"tailwind-merge": "^2.3.0",
|
||||
"tailwindcss-react-aria-components": "^1.1.1",
|
||||
"undici": "^6.10.2",
|
||||
"usehooks-ts": "^3.0.2",
|
||||
"yaml": "^2.4.1"
|
||||
"tailwindcss-react-aria-components": "^1.1.2",
|
||||
"undici": "^6.15.0",
|
||||
"usehooks-ts": "^3.1.0",
|
||||
"yaml": "^2.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@remix-run/dev": "^2.8.1",
|
||||
"@types/react": "^18.2.20",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@remix-run/dev": "^2.9.1",
|
||||
"@types/react": "^18.3.1",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-tale": "^1.0.16",
|
||||
"postcss": "^8.4.38",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"typescript": "^5.1.6",
|
||||
"vite": "^5.1.0",
|
||||
"typescript": "^5.4.5",
|
||||
"vite": "^5.2.11",
|
||||
"vite-tsconfig-paths": "^4.2.1"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
Generated
+1560
-1419
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user