import { Link, Outlet, createRootRoute, createRoute, createRouter } from "@tanstack/react-router"; import { useState, useTransition } from "react"; import { useFateClient, useLiveListView, useLiveView, useRequest, view, type ViewRef, } from "react-fate"; import type { FateMachine, FateUser } from "../server/fate"; const UserView = view()({ displayName: true, email: true, id: true, name: true, }); const MachineView = view()({ expiry: true, givenName: true, id: true, ipAddresses: true, lastSeen: true, name: true, online: true, tags: true, user: { displayName: true, id: true, name: true, }, }); const MachineConnectionView = { args: { first: 100 }, items: { cursor: true, node: MachineView, }, pagination: { hasNext: true, hasPrevious: true, nextCursor: true, previousCursor: true, }, }; const UserConnectionView = { args: { first: 100 }, items: { cursor: true, node: UserView, }, pagination: { hasNext: true, hasPrevious: true, nextCursor: true, previousCursor: true, }, }; const rootRoute = createRootRoute({ component: RootLayout, }); const indexRoute = createRoute({ getParentRoute: () => rootRoute, path: "/", component: HomePage, }); const machinesRoute = createRoute({ getParentRoute: () => rootRoute, path: "/machines", component: MachinesPage, }); const usersRoute = createRoute({ getParentRoute: () => rootRoute, path: "/users", component: UsersPage, }); const routeTree = rootRoute.addChildren([indexRoute, machinesRoute, usersRoute]); export const router = createRouter({ basepath: __PREFIX__, routeTree, }); declare module "@tanstack/react-router" { interface Register { router: typeof router; } } function RootLayout() { return (
); } function HomePage() { return (

Headplane SPA

This is the one-way SPA shell. Data should move to raw Fate views and actions.

); } function MachinesPage() { const { machines } = useRequest({ machines: { list: MachineConnectionView } }); const [machineItems, loadNext] = useLiveListView(MachineConnectionView, machines); return (

Machines

{machineItems.length === 0 ? (

No machines returned from Headscale.

) : (
{machineItems.map(({ node: machine }) => ( ))}
Machine User Addresses Status Last seen
)} {loadNext ? ( ) : null}
); } function MachineRow({ machine: machineRef }: { machine: ViewRef<"Machine"> }) { const fate = useFateClient(); const machine = useLiveView(MachineView, machineRef); const currentName = machine.givenName || machine.name; const [name, setName] = useState(currentName); const [error, setError] = useState(null); const [isPending, startTransition] = useTransition(); const rename = () => { const nextName = name.trim(); if (!nextName || nextName === currentName) { return; } startTransition(() => { void (async () => { setError(null); const result = await fate.mutations.machine.rename({ input: { id: machine.id, name: nextName }, view: MachineView, }); if (result.error) { setError(result.error.message); return; } setName(nextName); })(); }); }; return (
{currentName}
{machine.id}
{ event.preventDefault(); rename(); }} > setName(event.currentTarget.value)} value={name} />
{error ?
{error}
: null} {machine.tags.length > 0 ? (
{machine.tags.map((tag) => ( {tag} ))}
) : null} {machine.user ? machine.user.displayName || machine.user.name : "Tag-owned"}
{machine.ipAddresses.map((ip) => ( {ip} ))}
{machine.online ? "Online" : "Offline"} {formatDate(machine.lastSeen)} ); } function UsersPage() { const { users } = useRequest({ users: { list: UserConnectionView } }); const [userItems, loadNext] = useLiveListView(UserConnectionView, users); return (

Users

{userItems.length === 0 ? (

No users returned from Headscale.

) : (
{userItems.map(({ node: user }) => ( ))}
)} {loadNext ? ( ) : null}
); } function UserCard({ user: userRef }: { user: ViewRef<"User"> }) { const user = useLiveView(UserView, userRef); return (
{user.displayName || user.name}
{user.name}
{user.email ?
{user.email}
: null}
{user.id}
); } function formatDate(value: string | null | undefined) { if (!value) return "Never"; const date = new Date(value); if (Number.isNaN(date.getTime())) return value; return date.toLocaleString(); }