mirror of
https://github.com/tale/headplane.git
synced 2026-09-04 19:25:43 +00:00
chore: switch to react-router v7
This commit is contained in:
@@ -1,168 +1,174 @@
|
||||
import { LoaderFunctionArgs, ActionFunctionArgs } from '@remix-run/node'
|
||||
import { useLoaderData } from '@remix-run/react'
|
||||
import { useLiveData } from '~/utils/useLiveData'
|
||||
import { getSession } from '~/utils/sessions'
|
||||
import { Link as RemixLink } from '@remix-run/react'
|
||||
import { PreAuthKey, User } from '~/types'
|
||||
import { pull, post } from '~/utils/headscale'
|
||||
import { loadContext } from '~/utils/config/headplane'
|
||||
import { useState } from 'react'
|
||||
import { send } from '~/utils/res'
|
||||
import { LoaderFunctionArgs, ActionFunctionArgs } from 'react-router';
|
||||
import { useLoaderData } from 'react-router';
|
||||
import { useLiveData } from '~/utils/useLiveData';
|
||||
import { getSession } from '~/utils/sessions';
|
||||
import { Link as RemixLink } from 'react-router';
|
||||
import { PreAuthKey, User } from '~/types';
|
||||
import { pull, post } from '~/utils/headscale';
|
||||
import { loadContext } from '~/utils/config/headplane';
|
||||
import { useState } from 'react';
|
||||
import { send } from '~/utils/res';
|
||||
|
||||
import Link from '~/components/Link'
|
||||
import TableList from '~/components/TableList'
|
||||
import Select from '~/components/Select'
|
||||
import Switch from '~/components/Switch'
|
||||
import Link from '~/components/Link';
|
||||
import TableList from '~/components/TableList';
|
||||
import Select from '~/components/Select';
|
||||
import Switch from '~/components/Switch';
|
||||
|
||||
import AddPreAuthKey from './dialogs/new'
|
||||
import AuthKeyRow from './components/key'
|
||||
import AddPreAuthKey from './dialogs/new';
|
||||
import AuthKeyRow from './components/key';
|
||||
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
const session = await getSession(request.headers.get('Cookie'))
|
||||
const session = await getSession(request.headers.get('Cookie'));
|
||||
if (!session.has('hsApiKey')) {
|
||||
return send({ message: 'Unauthorized' }, {
|
||||
status: 401,
|
||||
})
|
||||
return send(
|
||||
{ message: 'Unauthorized' },
|
||||
{
|
||||
status: 401,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const data = await request.formData()
|
||||
const data = await request.formData();
|
||||
|
||||
// Expiring a pre-auth key
|
||||
if (request.method === 'DELETE') {
|
||||
const key = data.get('key')
|
||||
const user = data.get('user')
|
||||
const key = data.get('key');
|
||||
const user = data.get('user');
|
||||
|
||||
if (!key || !user) {
|
||||
return send({ message: 'Missing parameters' }, {
|
||||
status: 400,
|
||||
})
|
||||
return send(
|
||||
{ message: 'Missing parameters' },
|
||||
{
|
||||
status: 400,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
await post<{ preAuthKey: PreAuthKey }>(
|
||||
'v1/preauthkey/expire',
|
||||
session.get('hsApiKey')!,
|
||||
session.get('hsApiKey')!,
|
||||
{
|
||||
user: user,
|
||||
key: key,
|
||||
}
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
return { message: 'Pre-auth key expired' }
|
||||
return { message: 'Pre-auth key expired' };
|
||||
}
|
||||
|
||||
// Creating a new pre-auth key
|
||||
if (request.method === 'POST') {
|
||||
const user = data.get('user')
|
||||
const expiry = data.get('expiry')
|
||||
const reusable = data.get('reusable')
|
||||
const ephemeral = data.get('ephemeral')
|
||||
const user = data.get('user');
|
||||
const expiry = data.get('expiry');
|
||||
const reusable = data.get('reusable');
|
||||
const ephemeral = data.get('ephemeral');
|
||||
|
||||
if (!user || !expiry || !reusable || !ephemeral) {
|
||||
return send({ message: 'Missing parameters' }, {
|
||||
status: 400,
|
||||
})
|
||||
return send(
|
||||
{ message: 'Missing parameters' },
|
||||
{
|
||||
status: 400,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Extract the first "word" from expiry which is the day number
|
||||
// Calculate the date X days from now using the day number
|
||||
const day = Number(expiry.toString().split(' ')[0])
|
||||
const date = new Date()
|
||||
date.setDate(date.getDate() + day)
|
||||
const day = Number(expiry.toString().split(' ')[0]);
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() + day);
|
||||
|
||||
const key = await post<{ preAuthKey: PreAuthKey }>(
|
||||
'v1/preauthkey',
|
||||
session.get('hsApiKey')!,
|
||||
session.get('hsApiKey')!,
|
||||
{
|
||||
user: user,
|
||||
ephemeral: ephemeral === 'on',
|
||||
reusable: reusable === 'on',
|
||||
expiration: date.toISOString(),
|
||||
aclTags: [], // TODO
|
||||
}
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
return { message: 'Pre-auth key created', key }
|
||||
return { message: 'Pre-auth key created', key };
|
||||
}
|
||||
}
|
||||
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const context = await loadContext()
|
||||
const session = await getSession(request.headers.get('Cookie'))
|
||||
const users = await pull<{ users: User[] }>('v1/user', session.get('hsApiKey')!)
|
||||
const context = await loadContext();
|
||||
const session = await getSession(request.headers.get('Cookie'));
|
||||
const users = await pull<{ users: User[] }>(
|
||||
'v1/user',
|
||||
session.get('hsApiKey')!,
|
||||
);
|
||||
|
||||
const preAuthKeys = await Promise.all(users.users.map(user => {
|
||||
const qp = new URLSearchParams()
|
||||
qp.set('user', user.name)
|
||||
const preAuthKeys = await Promise.all(
|
||||
users.users.map((user) => {
|
||||
const qp = new URLSearchParams();
|
||||
qp.set('user', user.name);
|
||||
|
||||
return pull<{ preAuthKeys: PreAuthKey[] }>(
|
||||
`v1/preauthkey?${qp.toString()}`,
|
||||
session.get('hsApiKey')!
|
||||
)
|
||||
}))
|
||||
return pull<{ preAuthKeys: PreAuthKey[] }>(
|
||||
`v1/preauthkey?${qp.toString()}`,
|
||||
session.get('hsApiKey')!,
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
keys: preAuthKeys.flatMap(keys => keys.preAuthKeys),
|
||||
keys: preAuthKeys.flatMap((keys) => keys.preAuthKeys),
|
||||
users: users.users,
|
||||
server: context.headscalePublicUrl ?? context.headscaleUrl,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
const { keys, users, server } = useLoaderData<typeof loader>()
|
||||
const [user, setUser] = useState('All')
|
||||
const [status, setStatus] = useState('Active')
|
||||
useLiveData({ interval: 3000 })
|
||||
const { keys, users, server } = useLoaderData<typeof loader>();
|
||||
const [user, setUser] = useState('All');
|
||||
const [status, setStatus] = useState('Active');
|
||||
useLiveData({ interval: 3000 });
|
||||
|
||||
const filteredKeys = keys.filter(key => {
|
||||
const filteredKeys = keys.filter((key) => {
|
||||
if (user !== 'All' && key.user !== user) {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
|
||||
if (status !== 'All') {
|
||||
const now = new Date()
|
||||
const expiry = new Date(key.expiration)
|
||||
const now = new Date();
|
||||
const expiry = new Date(key.expiration);
|
||||
|
||||
if (status === 'Active') {
|
||||
return !(expiry < now) && !key.used
|
||||
return !(expiry < now) && !key.used;
|
||||
}
|
||||
|
||||
if (status === 'Used/Expired') {
|
||||
return key.used || expiry < now
|
||||
return key.used || expiry < now;
|
||||
}
|
||||
|
||||
if (status === 'Reusable') {
|
||||
return key.reusable
|
||||
return key.reusable;
|
||||
}
|
||||
|
||||
if (status === 'Ephemeral') {
|
||||
return key.ephemeral
|
||||
return key.ephemeral;
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
return true;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className='flex flex-col w-2/3'>
|
||||
<div className="flex flex-col w-2/3">
|
||||
<p className="mb-8 text-md">
|
||||
<RemixLink
|
||||
to="/settings"
|
||||
className="font-medium"
|
||||
>
|
||||
<RemixLink to="/settings" className="font-medium">
|
||||
Settings
|
||||
</RemixLink>
|
||||
<span className="mx-2">
|
||||
/
|
||||
</span>
|
||||
{' '}
|
||||
Pre-Auth Keys
|
||||
<span className="mx-2">/</span> Pre-Auth Keys
|
||||
</p>
|
||||
<h1 className='text-2xl font-medium mb-4'>Pre-Auth Keys</h1>
|
||||
<h1 className="text-2xl font-medium mb-4">Pre-Auth Keys</h1>
|
||||
<p className="text-gray-700 dark:text-gray-300 mb-4">
|
||||
Headscale fully supports pre-authentication keys in order to
|
||||
easily add devices to your Tailnet.
|
||||
To learn more about using pre-authentication keys, visit the
|
||||
{' '}
|
||||
Headscale fully supports pre-authentication keys in order to easily add
|
||||
devices to your Tailnet. To learn more about using pre-authentication
|
||||
keys, visit the{' '}
|
||||
<Link
|
||||
to="https://tailscale.com/kb/1085/auth-keys/"
|
||||
name="Tailscale Auth Keys documentation"
|
||||
@@ -182,7 +188,7 @@ export default function Page() {
|
||||
state={[user, setUser]}
|
||||
>
|
||||
<Select.Item id="All">All</Select.Item>
|
||||
{users.map(user => (
|
||||
{users.map((user) => (
|
||||
<Select.Item key={user.id} id={user.name}>
|
||||
{user.name}
|
||||
</Select.Item>
|
||||
@@ -209,16 +215,16 @@ export default function Page() {
|
||||
<TableList className="mt-4">
|
||||
{filteredKeys.length === 0 ? (
|
||||
<TableList.Item>
|
||||
<p className="opacity-50 text-sm mx-auto">
|
||||
No pre-auth keys
|
||||
</p>
|
||||
<p className="opacity-50 text-sm mx-auto">No pre-auth keys</p>
|
||||
</TableList.Item>
|
||||
) : filteredKeys.map(key => (
|
||||
<TableList.Item key={key.id}>
|
||||
<AuthKeyRow authKey={key} server={server} />
|
||||
</TableList.Item>
|
||||
))}
|
||||
) : (
|
||||
filteredKeys.map((key) => (
|
||||
<TableList.Item key={key.id}>
|
||||
<AuthKeyRow authKey={key} server={server} />
|
||||
</TableList.Item>
|
||||
))
|
||||
)}
|
||||
</TableList>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import type { PreAuthKey } from '~/types'
|
||||
import { toast } from '~/components/Toaster'
|
||||
import type { PreAuthKey } from '~/types';
|
||||
import { toast } from '~/components/Toaster';
|
||||
|
||||
import Code from '~/components/Code'
|
||||
import Button from '~/components/Button'
|
||||
import Attribute from '~/components/Attribute'
|
||||
import ExpireKey from '../dialogs/expire'
|
||||
import Code from '~/components/Code';
|
||||
import Button from '~/components/Button';
|
||||
import Attribute from '~/components/Attribute';
|
||||
import ExpireKey from '../dialogs/expire';
|
||||
|
||||
interface Props {
|
||||
authKey: PreAuthKey
|
||||
server: string
|
||||
authKey: PreAuthKey;
|
||||
server: string;
|
||||
}
|
||||
|
||||
export default function AuthKeyRow({ authKey, server }: Props) {
|
||||
const createdAt = new Date(authKey.createdAt).toLocaleString()
|
||||
const expiration = new Date(authKey.expiration).toLocaleString()
|
||||
const createdAt = new Date(authKey.createdAt).toLocaleString();
|
||||
const expiration = new Date(authKey.expiration).toLocaleString();
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
@@ -31,25 +31,24 @@ export default function AuthKeyRow({ authKey, server }: Props) {
|
||||
tailscale up --login-server {server} --authkey {authKey.key}
|
||||
</Code>
|
||||
<div className="flex gap-4 items-center">
|
||||
{authKey.used || new Date(authKey.expiration) < new Date()
|
||||
? undefined
|
||||
: (
|
||||
<ExpireKey authKey={authKey} />
|
||||
)}
|
||||
{authKey.used ||
|
||||
new Date(authKey.expiration) < new Date() ? undefined : (
|
||||
<ExpireKey authKey={authKey} />
|
||||
)}
|
||||
<Button
|
||||
variant="light"
|
||||
variant="light"
|
||||
className="my-4"
|
||||
onPress={async () => {
|
||||
await navigator.clipboard.writeText(
|
||||
`tailscale up --login-server ${server} --authkey ${authKey.key}`
|
||||
)
|
||||
`tailscale up --login-server ${server} --authkey ${authKey.key}`,
|
||||
);
|
||||
|
||||
toast('Copied command to clipboard')
|
||||
toast('Copied command to clipboard');
|
||||
}}
|
||||
>
|
||||
Copy Tailscale Command
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,45 +1,40 @@
|
||||
import { useFetcher } from '@remix-run/react'
|
||||
import type { PreAuthKey } from '~/types'
|
||||
import { cn } from '~/utils/cn'
|
||||
import { useFetcher } from 'react-router';
|
||||
import type { PreAuthKey } from '~/types';
|
||||
import { cn } from '~/utils/cn';
|
||||
|
||||
import Dialog from '~/components/Dialog'
|
||||
import Spinner from '~/components/Spinner'
|
||||
import Dialog from '~/components/Dialog';
|
||||
import Spinner from '~/components/Spinner';
|
||||
|
||||
interface Props {
|
||||
authKey: PreAuthKey
|
||||
authKey: PreAuthKey;
|
||||
}
|
||||
|
||||
export default function ExpireKey({ authKey }: Props) {
|
||||
const fetcher = useFetcher()
|
||||
const fetcher = useFetcher();
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<Dialog.Button className="my-4">
|
||||
Expire Key
|
||||
</Dialog.Button>
|
||||
<Dialog.Button className="my-4">Expire Key</Dialog.Button>
|
||||
<Dialog.Panel>
|
||||
{close => (
|
||||
{(close) => (
|
||||
<>
|
||||
<Dialog.Title>
|
||||
Expire auth key?
|
||||
</Dialog.Title>
|
||||
<fetcher.Form method="DELETE" onSubmit={e => {
|
||||
fetcher.submit(e.currentTarget)
|
||||
close()
|
||||
}}>
|
||||
<Dialog.Title>Expire auth key?</Dialog.Title>
|
||||
<fetcher.Form
|
||||
method="DELETE"
|
||||
onSubmit={(e) => {
|
||||
fetcher.submit(e.currentTarget);
|
||||
close();
|
||||
}}
|
||||
>
|
||||
<input type="hidden" name="user" value={authKey.user} />
|
||||
<input type="hidden" name="key" value={authKey.key} />
|
||||
<Dialog.Text>
|
||||
Expiring this authentication key will immediately
|
||||
prevent it from being used to authenticate new devices.
|
||||
{' '}
|
||||
This action cannot be undone.
|
||||
Expiring this authentication key will immediately prevent it
|
||||
from being used to authenticate new devices. This action cannot
|
||||
be undone.
|
||||
</Dialog.Text>
|
||||
<div className="mt-6 flex justify-end gap-2 mt-6">
|
||||
<Dialog.Action
|
||||
variant="cancel"
|
||||
onPress={close}
|
||||
>
|
||||
<Dialog.Action variant="cancel" onPress={close}>
|
||||
Cancel
|
||||
</Dialog.Action>
|
||||
<Dialog.Action
|
||||
@@ -52,11 +47,9 @@ export default function ExpireKey({ authKey }: Props) {
|
||||
)}
|
||||
onPress={close}
|
||||
>
|
||||
{fetcher.state === 'idle'
|
||||
? undefined
|
||||
: (
|
||||
<Spinner className="w-3 h-3" />
|
||||
)}
|
||||
{fetcher.state === 'idle' ? undefined : (
|
||||
<Spinner className="w-3 h-3" />
|
||||
)}
|
||||
Expire
|
||||
</Dialog.Action>
|
||||
</div>
|
||||
@@ -65,5 +58,5 @@ export default function ExpireKey({ authKey }: Props) {
|
||||
)}
|
||||
</Dialog.Panel>
|
||||
</Dialog>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,50 +1,47 @@
|
||||
import { RepoForkedIcon } from '@primer/octicons-react'
|
||||
import { useFetcher } from '@remix-run/react'
|
||||
import { useState } from 'react'
|
||||
import { RepoForkedIcon } from '@primer/octicons-react';
|
||||
import { useFetcher } from 'react-router';
|
||||
import { useState } from 'react';
|
||||
|
||||
import Dialog from '~/components/Dialog'
|
||||
import TextField from '~/components/TextField'
|
||||
import NumberField from '~/components/NumberField'
|
||||
import Tooltip from '~/components/Tooltip'
|
||||
import Select from '~/components/Select'
|
||||
import Switch from '~/components/Switch'
|
||||
import Link from '~/components/Link'
|
||||
import Spinner from '~/components/Spinner'
|
||||
import Dialog from '~/components/Dialog';
|
||||
import TextField from '~/components/TextField';
|
||||
import NumberField from '~/components/NumberField';
|
||||
import Tooltip from '~/components/Tooltip';
|
||||
import Select from '~/components/Select';
|
||||
import Switch from '~/components/Switch';
|
||||
import Link from '~/components/Link';
|
||||
import Spinner from '~/components/Spinner';
|
||||
|
||||
import { cn } from '~/utils/cn'
|
||||
import { User } from '~/types'
|
||||
import { cn } from '~/utils/cn';
|
||||
import { User } from '~/types';
|
||||
|
||||
interface Props {
|
||||
users: User[]
|
||||
users: User[];
|
||||
}
|
||||
|
||||
// TODO: Tags
|
||||
export default function AddPreAuthKey(data: Props) {
|
||||
const fetcher = useFetcher()
|
||||
const [user, setUser] = useState('')
|
||||
const [reusable, setReusable] = useState(false)
|
||||
const [ephemeral, setEphemeral] = useState(false)
|
||||
const [aclTags, setAclTags] = useState([])
|
||||
const [expiry, setExpiry] = useState(90)
|
||||
const fetcher = useFetcher();
|
||||
const [user, setUser] = useState('');
|
||||
const [reusable, setReusable] = useState(false);
|
||||
const [ephemeral, setEphemeral] = useState(false);
|
||||
const [aclTags, setAclTags] = useState([]);
|
||||
const [expiry, setExpiry] = useState(90);
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<Dialog.Button className="my-4">
|
||||
Create pre-auth key
|
||||
</Dialog.Button>
|
||||
<Dialog.Button className="my-4">Create pre-auth key</Dialog.Button>
|
||||
<Dialog.Panel>
|
||||
{close => (
|
||||
{(close) => (
|
||||
<>
|
||||
<Dialog.Title>
|
||||
Generate auth key
|
||||
</Dialog.Title>
|
||||
<fetcher.Form method="POST" onSubmit={e => {
|
||||
fetcher.submit(e.currentTarget)
|
||||
close()
|
||||
}}>
|
||||
<Dialog.Text className="font-semibold">
|
||||
User
|
||||
</Dialog.Text>
|
||||
<Dialog.Title>Generate auth key</Dialog.Title>
|
||||
<fetcher.Form
|
||||
method="POST"
|
||||
onSubmit={(e) => {
|
||||
fetcher.submit(e.currentTarget);
|
||||
close();
|
||||
}}
|
||||
>
|
||||
<Dialog.Text className="font-semibold">User</Dialog.Text>
|
||||
<Dialog.Text className="text-sm">
|
||||
Attach this key to a user
|
||||
</Dialog.Text>
|
||||
@@ -54,7 +51,7 @@ export default function AddPreAuthKey(data: Props) {
|
||||
placeholder="Select a user"
|
||||
state={[user, setUser]}
|
||||
>
|
||||
{data.users.map(user => (
|
||||
{data.users.map((user) => (
|
||||
<Select.Item key={user.id} id={user.name}>
|
||||
{user.name}
|
||||
</Select.Item>
|
||||
@@ -80,9 +77,7 @@ export default function AddPreAuthKey(data: Props) {
|
||||
/>
|
||||
<div className="flex justify-between items-center mt-6">
|
||||
<div>
|
||||
<Dialog.Text className="font-semibold">
|
||||
Reusable
|
||||
</Dialog.Text>
|
||||
<Dialog.Text className="font-semibold">Reusable</Dialog.Text>
|
||||
<Dialog.Text className="text-sm">
|
||||
Use this key to authenticate more than one device.
|
||||
</Dialog.Text>
|
||||
@@ -91,19 +86,22 @@ export default function AddPreAuthKey(data: Props) {
|
||||
label="Reusable"
|
||||
name="reusable"
|
||||
defaultSelected={reusable}
|
||||
onChange={() => { setReusable(!reusable) }}
|
||||
onChange={() => {
|
||||
setReusable(!reusable);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<input type="hidden" name="reusable" value={reusable.toString()} />
|
||||
<input
|
||||
type="hidden"
|
||||
name="reusable"
|
||||
value={reusable.toString()}
|
||||
/>
|
||||
<div className="flex justify-between items-center mt-6">
|
||||
<div>
|
||||
<Dialog.Text className="font-semibold">
|
||||
Ephemeral
|
||||
</Dialog.Text>
|
||||
<Dialog.Text className="font-semibold">Ephemeral</Dialog.Text>
|
||||
<Dialog.Text className="text-sm">
|
||||
Devices authenticated with this key will
|
||||
be automatically removed once they go offline.
|
||||
{' '}
|
||||
Devices authenticated with this key will be automatically
|
||||
removed once they go offline.{' '}
|
||||
<Link
|
||||
to="https://tailscale.com/kb/1111/ephemeral-nodes"
|
||||
name="Tailscale Ephemeral Nodes Documentation"
|
||||
@@ -117,16 +115,17 @@ export default function AddPreAuthKey(data: Props) {
|
||||
name="ephemeral"
|
||||
defaultSelected={ephemeral}
|
||||
onChange={() => {
|
||||
setEphemeral(!ephemeral)
|
||||
setEphemeral(!ephemeral);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<input type="hidden" name="ephemeral" value={ephemeral.toString()} />
|
||||
<input
|
||||
type="hidden"
|
||||
name="ephemeral"
|
||||
value={ephemeral.toString()}
|
||||
/>
|
||||
<div className="mt-6 flex justify-end gap-2 mt-6">
|
||||
<Dialog.Action
|
||||
variant="cancel"
|
||||
onPress={close}
|
||||
>
|
||||
<Dialog.Action variant="cancel" onPress={close}>
|
||||
Cancel
|
||||
</Dialog.Action>
|
||||
<Dialog.Action
|
||||
@@ -134,11 +133,9 @@ export default function AddPreAuthKey(data: Props) {
|
||||
onPress={close}
|
||||
isDisabled={!user || !expiry}
|
||||
>
|
||||
{fetcher.state === 'idle'
|
||||
? undefined
|
||||
: (
|
||||
<Spinner className="w-3 h-3" />
|
||||
)}
|
||||
{fetcher.state === 'idle' ? undefined : (
|
||||
<Spinner className="w-3 h-3" />
|
||||
)}
|
||||
Generate
|
||||
</Dialog.Action>
|
||||
</div>
|
||||
@@ -147,5 +144,5 @@ export default function AddPreAuthKey(data: Props) {
|
||||
)}
|
||||
</Dialog.Panel>
|
||||
</Dialog>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,28 +1,26 @@
|
||||
import Link from '~/components/Link'
|
||||
import Button from '~/components/Button'
|
||||
import { Link as RemixLink } from '@remix-run/react'
|
||||
import { ArrowRightIcon } from '@primer/octicons-react'
|
||||
import { cn } from '~/utils/cn'
|
||||
import Link from '~/components/Link';
|
||||
import Button from '~/components/Button';
|
||||
import { Link as RemixLink } from 'react-router';
|
||||
import { ArrowRightIcon } from '@primer/octicons-react';
|
||||
import { cn } from '~/utils/cn';
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div className="flex flex-col gap-8 max-w-screen-lg">
|
||||
<div className='flex flex-col w-2/3'>
|
||||
<h1 className='text-2xl font-medium mb-4'>Settings</h1>
|
||||
<div className="flex flex-col w-2/3">
|
||||
<h1 className="text-2xl font-medium mb-4">Settings</h1>
|
||||
<p className="text-gray-700 dark:text-gray-300">
|
||||
The settings page is still under construction.
|
||||
As I'm able to add more features, I'll be adding them here.
|
||||
If you require any features, feel free to open an issue on
|
||||
the GitHub repository.
|
||||
The settings page is still under construction. As I'm able to add more
|
||||
features, I'll be adding them here. If you require any features, feel
|
||||
free to open an issue on the GitHub repository.
|
||||
</p>
|
||||
</div>
|
||||
<div className='flex flex-col w-2/3'>
|
||||
<h1 className='text-2xl font-medium mb-4'>Pre-Auth Keys</h1>
|
||||
<div className="flex flex-col w-2/3">
|
||||
<h1 className="text-2xl font-medium mb-4">Pre-Auth Keys</h1>
|
||||
<p className="text-gray-700 dark:text-gray-300">
|
||||
Headscale fully supports pre-authentication keys in order to
|
||||
easily add devices to your Tailnet.
|
||||
To learn more about using pre-authentication keys, visit the
|
||||
{' '}
|
||||
Headscale fully supports pre-authentication keys in order to easily
|
||||
add devices to your Tailnet. To learn more about using
|
||||
pre-authentication keys, visit the{' '}
|
||||
<Link
|
||||
to="https://tailscale.com/kb/1085/auth-keys/"
|
||||
name="Tailscale Auth Keys documentation"
|
||||
@@ -32,14 +30,16 @@ export default function Page() {
|
||||
</p>
|
||||
</div>
|
||||
<RemixLink to="/settings/auth-keys">
|
||||
<span className={cn(
|
||||
'text-lg font-medium',
|
||||
'text-gray-700 dark:text-gray-300',
|
||||
)}>
|
||||
<span
|
||||
className={cn(
|
||||
'text-lg font-medium',
|
||||
'text-gray-700 dark:text-gray-300',
|
||||
)}
|
||||
>
|
||||
Manage Auth Keys
|
||||
<ArrowRightIcon className="w-5 h-5 ml-2" />
|
||||
</span>
|
||||
</RemixLink>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user