177 lines
4.7 KiB
TypeScript
177 lines
4.7 KiB
TypeScript
import { useFetcher } from 'react-router';
|
|
import { Dispatch, SetStateAction, useMemo } from 'react';
|
|
|
|
import Dialog from '~/components/Dialog';
|
|
import Switch from '~/components/Switch';
|
|
import Link from '~/components/Link';
|
|
import type { Machine, Route } from '~/types';
|
|
import { cn } from '~/utils/cn';
|
|
|
|
interface RoutesProps {
|
|
readonly machine: Machine;
|
|
readonly routes: Route[];
|
|
readonly state: [boolean, Dispatch<SetStateAction<boolean>>];
|
|
}
|
|
|
|
// TODO: Support deleting routes
|
|
export default function Routes({ machine, routes, state }: RoutesProps) {
|
|
const fetcher = useFetcher();
|
|
|
|
// This is much easier with Object.groupBy but it's too new for us
|
|
const { exit, subnet } = routes.reduce<{
|
|
exit: Route[];
|
|
subnet: Route[];
|
|
}>(
|
|
(acc, route) => {
|
|
if (route.prefix === '::/0' || route.prefix === '0.0.0.0/0') {
|
|
acc.exit.push(route);
|
|
return acc;
|
|
}
|
|
|
|
acc.subnet.push(route);
|
|
return acc;
|
|
},
|
|
{ exit: [], subnet: [] },
|
|
);
|
|
|
|
const exitEnabled = useMemo(() => {
|
|
if (exit.length !== 2) return false;
|
|
return exit[0].enabled && exit[1].enabled;
|
|
}, [exit]);
|
|
|
|
return (
|
|
<Dialog>
|
|
<Dialog.Panel control={state}>
|
|
{(close) => (
|
|
<>
|
|
<Dialog.Title>
|
|
Edit route settings of {machine.givenName}
|
|
</Dialog.Title>
|
|
<Dialog.Text className="font-bold">Subnet routes</Dialog.Text>
|
|
<Dialog.Text>
|
|
Connect to devices you can't install Tailscale on by
|
|
advertising IP ranges as subnet routes.{' '}
|
|
<Link
|
|
to="https://tailscale.com/kb/1019/subnets"
|
|
name="Tailscale Subnets Documentation"
|
|
>
|
|
Learn More
|
|
</Link>
|
|
</Dialog.Text>
|
|
<div
|
|
className={cn(
|
|
'rounded-lg overflow-y-auto my-2',
|
|
'divide-y divide-zinc-200 dark:divide-zinc-700 align-top',
|
|
'border border-zinc-200 dark:border-zinc-700',
|
|
)}
|
|
>
|
|
{subnet.length === 0 ? (
|
|
<div
|
|
className={cn(
|
|
'flex py-4 px-4 bg-ui-100 dark:bg-ui-800',
|
|
'items-center justify-center',
|
|
'text-ui-600 dark:text-ui-300',
|
|
)}
|
|
>
|
|
<p>No routes are advertised on this machine.</p>
|
|
</div>
|
|
) : undefined}
|
|
{subnet.map((route) => (
|
|
<div
|
|
key={route.id}
|
|
className={cn(
|
|
'flex py-2 px-4 bg-ui-100 dark:bg-ui-800',
|
|
'items-center justify-between',
|
|
)}
|
|
>
|
|
<p>{route.prefix}</p>
|
|
<Switch
|
|
defaultSelected={route.enabled}
|
|
label="Enabled"
|
|
onChange={(checked) => {
|
|
const form = new FormData();
|
|
form.set('id', machine.id);
|
|
form.set('_method', 'routes');
|
|
form.set('route', route.id);
|
|
|
|
form.set('enabled', String(checked));
|
|
fetcher.submit(form, {
|
|
method: 'POST',
|
|
});
|
|
}}
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<Dialog.Text className="font-bold mt-8">Exit nodes</Dialog.Text>
|
|
<Dialog.Text>
|
|
Allow your network to route internet traffic through this machine.{' '}
|
|
<Link
|
|
to="https://tailscale.com/kb/1103/exit-nodes"
|
|
name="Tailscale Exit-node Documentation"
|
|
>
|
|
Learn More
|
|
</Link>
|
|
</Dialog.Text>
|
|
<div
|
|
className={cn(
|
|
'rounded-lg overflow-y-auto my-2',
|
|
'divide-y divide-zinc-200 dark:divide-zinc-700 align-top',
|
|
'border border-zinc-200 dark:border-zinc-700',
|
|
)}
|
|
>
|
|
{exit.length === 0 ? (
|
|
<div
|
|
className={cn(
|
|
'flex py-4 px-4 bg-ui-100 dark:bg-ui-800',
|
|
'items-center justify-center',
|
|
'text-ui-600 dark:text-ui-300',
|
|
)}
|
|
>
|
|
<p>This machine is not an exit node.</p>
|
|
</div>
|
|
) : (
|
|
<div
|
|
className={cn(
|
|
'flex py-2 px-4 bg-ui-100 dark:bg-ui-800',
|
|
'items-center justify-between',
|
|
)}
|
|
>
|
|
<p>Use as exit node</p>
|
|
<Switch
|
|
defaultSelected={exitEnabled}
|
|
label="Enabled"
|
|
onChange={(checked) => {
|
|
const form = new FormData();
|
|
form.set('id', machine.id);
|
|
form.set('_method', 'exit-node');
|
|
form.set(
|
|
'routes',
|
|
exit.map((route) => route.id).join(','),
|
|
);
|
|
|
|
form.set('enabled', String(checked));
|
|
fetcher.submit(form, {
|
|
method: 'POST',
|
|
});
|
|
}}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<Dialog.Gutter>
|
|
<Dialog.Action
|
|
variant="cancel"
|
|
isDisabled={fetcher.state === 'submitting'}
|
|
onPress={close}
|
|
>
|
|
Close
|
|
</Dialog.Action>
|
|
</Dialog.Gutter>
|
|
</>
|
|
)}
|
|
</Dialog.Panel>
|
|
</Dialog>
|
|
);
|
|
}
|