diff --git a/app/routes/_data.machines.$id/route.tsx b/app/routes/_data.machines.$id/route.tsx index 85b08be..62ad1c4 100644 --- a/app/routes/_data.machines.$id/route.tsx +++ b/app/routes/_data.machines.$id/route.tsx @@ -1,10 +1,14 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import { ActionFunctionArgs, LoaderFunctionArgs } from '@remix-run/node' -import { Link, useLoaderData } from '@remix-run/react' -import { useMemo } from 'react' +import { Link as RemixLink, useLoaderData } from '@remix-run/react' +import { InfoIcon, GearIcon, CheckCircleIcon, SkipIcon, PersonIcon } from '@primer/octicons-react' +import { useMemo, useState } from 'react' import Attribute from '~/components/Attribute' +import Button from '~/components/Button' import Card from '~/components/Card' +import Menu from '~/components/Menu' +import Tooltip from '~/components/Tooltip' import StatusCircle from '~/components/StatusCircle' import { Machine, Route, User } from '~/types' import { cn } from '~/utils/cn' @@ -13,9 +17,11 @@ import { loadConfig } from '~/utils/config/headscale' import { pull } from '~/utils/headscale' import { getSession } from '~/utils/sessions' import { useLiveData } from '~/utils/useLiveData' +import Link from '~/components/Link' import { menuAction } from '../_data.machines._index/action' import MenuOptions from '../_data.machines._index/menu' +import Routes from '../_data.machines._index/dialogs/routes' export async function loader({ request, params }: LoaderFunctionArgs) { const session = await getSession(request.headers.get('Cookie')) @@ -53,6 +59,7 @@ export async function action({ request }: ActionFunctionArgs) { export default function Page() { const { machine, magic, routes, users } = useLoaderData() + const routesState = useState(false) useLiveData({ interval: 1000 }) const expired = machine.expiry === '0001-01-01 00:00:00' @@ -71,36 +78,52 @@ export default function Page() { } // This is much easier with Object.groupBy but it's too new for us - const { exit, subnet } = routes.reduce((acc, route) => { + const { exit, subnet, subnetApproved } = routes.reduce((acc, route) => { if (route.prefix === '::/0' || route.prefix === '0.0.0.0/0') { acc.exit.push(route) return acc } + if (route.enabled) { + acc.subnetApproved.push(route) + return acc + } + acc.subnet.push(route) return acc - }, { exit: [], subnet: [] }) + }, { exit: [], subnetApproved: [], subnet: [] }) const exitEnabled = useMemo(() => { if (exit.length !== 2) return false return exit[0].enabled && exit[1].enabled }, [exit]) + if (exitEnabled) { + tags.unshift('Exit Node') + } + + if (subnetApproved.length > 0) { + tags.unshift('Subnets') + } + return (

- All Machines - + / {machine.givenName}

-
+

{machine.givenName} @@ -109,26 +132,227 @@ export default function Page() { + + Machine Settings + + } />

-
- {tags.map(tag => ( - - {tag} +
+
+ + Managed by + + + + + + By default, a machine’s permissions match its creator’s. + + - ))} +
+
+ +
+ {machine.user.name} +
+
+
+

+ Status +

+
+ {tags.map(tag => ( + + {tag} + + ))} +
+
+

+ Subnets & Routing +

+ +
+

+ Subnets let you expose physical network routes onto Tailscale. + {' '} + + Learn More + +

+ +
+ +
+ + Approved + + + + + + Traffic to these routes are being + routed through this machine. + + + +
+ {subnetApproved.length === 0 ? ( + + — + + ) : ( +
    + {subnetApproved.map(route => ( +
  • + {route.prefix} +
  • + ))} +
+ )} +
+ +
+
+ + Awaiting Approval + + + + + + This machine is advertising these routes, + but they must be approved before traffic + will be routed to them. + + + +
+ {subnet.length === 0 ? ( + + — + + ) : ( +
    + {subnet.map(route => ( +
  • + {route.prefix} +
  • + ))} +
+ )} +
+ +
+
+ + Exit Node + + + + + + Whether this machine can act as an + exit node for your tailnet. + + + +
+ {exit.length === 0 ? ( + + — + + ) : exitEnabled ? ( + + + Allowed + + ) : ( + + + Awaiting Approval + + )} +
+ +
+

Machine Details

@@ -167,77 +391,6 @@ export default function Page() { ) : undefined} -

- Routing & Subnets -

- {exit.length > 0 - ? ( -
- -

- Exit Node -

- - {exitEnabled ? 'Enabled' : 'Disabled'} - - -
- ) : undefined} - - {subnet.length === 0 - ? ( -
-

- No routes are advertised on this machine. -

-
- ) - : subnet.map((route, i) => ( -
-
-

- {route.prefix} -

-

- {' '} - (Created: - {' '} - {new Date(route.createdAt).toLocaleString()} - ) -

-
-
- -
-

- {route.enabled ? 'Enabled' : 'Disabled'} -

-

- {route.isPrimary ? 'Primary' : 'Secondary'} -

-
- -
-
-
- ))} -
) } diff --git a/app/routes/_data.machines._index/dialogs/routes.tsx b/app/routes/_data.machines._index/dialogs/routes.tsx index a0f7fe1..7414036 100644 --- a/app/routes/_data.machines._index/dialogs/routes.tsx +++ b/app/routes/_data.machines._index/dialogs/routes.tsx @@ -80,7 +80,7 @@ export default function Routes({ machine, routes, state }: RoutesProps) { : undefined} {subnet.map(route => (
{ + if (route.prefix === '::/0' || route.prefix === '0.0.0.0/0') { + acc.exit.push(route) + return acc + } + + if (route.enabled) { + acc.subnetApproved.push(route) + return acc + } + + acc.subnet.push(route) + return acc + }, { exit: [], subnetApproved: [], subnet: [] }) + + const exitEnabled = useMemo(() => { + if (exit.length !== 2) return false + return exit[0].enabled && exit[1].enabled + }, [exit]) + + if (exitEnabled) { + tags.unshift('Exit Node') + } + + if (subnetApproved.length > 0) { + tags.unshift('Subnets') + } + return ( - - - + {buttonChild ?? ( + + + + )} Edit machine name