mirror of
https://github.com/tale/headplane.git
synced 2026-08-31 09:18:29 +00:00
feat: begin support for components and dark mode
This commit is contained in:
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user