From 252e78d61849bb1dfa42cab0943163db47aedec4 Mon Sep 17 00:00:00 2001 From: Aarnav Tale Date: Mon, 22 Apr 2024 14:33:51 -0400 Subject: [PATCH] feat: facelift overall machines page and refactor --- app/components/Modal.tsx | 2 + app/routes/_data.acls._index/route.tsx | 2 +- app/routes/_data.machines._index.tsx | 208 --- app/routes/_data.machines._index/machine.tsx | 204 +++ app/routes/_data.machines._index/route.tsx | 121 ++ app/routes/_data.tsx | 2 +- package.json | 1 + pnpm-lock.yaml | 1446 ++++++++++++++++++ tailwind.config.ts | 11 + 9 files changed, 1787 insertions(+), 210 deletions(-) delete mode 100644 app/routes/_data.machines._index.tsx create mode 100644 app/routes/_data.machines._index/machine.tsx create mode 100644 app/routes/_data.machines._index/route.tsx diff --git a/app/components/Modal.tsx b/app/components/Modal.tsx index ea58835..e6ba117 100644 --- a/app/components/Modal.tsx +++ b/app/components/Modal.tsx @@ -26,6 +26,8 @@ type Properties = { readonly parameters?: HookParameters; } +export type OpenFunction = (overrides?: Overrides) => void + export default function useModal(properties?: HookParameters) { const [isOpen, setIsOpen] = useState(false) const [liveProperties, setLiveProperties] = useState(properties) diff --git a/app/routes/_data.acls._index/route.tsx b/app/routes/_data.acls._index/route.tsx index 141fe64..e3661ac 100644 --- a/app/routes/_data.acls._index/route.tsx +++ b/app/routes/_data.acls._index/route.tsx @@ -59,7 +59,7 @@ export default function Page() { const [acl, setAcl] = useState(data.currentAcl) return ( -
+
{data.hasAclWrite ? undefined : (
diff --git a/app/routes/_data.machines._index.tsx b/app/routes/_data.machines._index.tsx deleted file mode 100644 index 8af7bad..0000000 --- a/app/routes/_data.machines._index.tsx +++ /dev/null @@ -1,208 +0,0 @@ -/* eslint-disable unicorn/filename-case */ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ - -import { ClipboardIcon, EllipsisHorizontalIcon } from '@heroicons/react/24/outline' -import { type ActionFunctionArgs, json, type LoaderFunctionArgs } from '@remix-run/node' -import { Link, useFetcher, useLoaderData } from '@remix-run/react' -import clsx from 'clsx' -import { useState } from 'react' -import { toast } from 'react-hot-toast/headless' - -import Dropdown from '~/components/Dropdown' -import useModal from '~/components/Modal' -import StatusCircle from '~/components/StatusCircle' -import { type Machine } from '~/types' -import { del, pull } from '~/utils/headscale' -import { getSession } from '~/utils/sessions' -import { useLiveData } from '~/utils/useLiveData' - -export async function loader({ request }: LoaderFunctionArgs) { - const session = await getSession(request.headers.get('Cookie')) - - const data = await pull<{ nodes: Machine[] }>('v1/node', session.get('hsApiKey')!) - return data.nodes -} - -export async function action({ request }: ActionFunctionArgs) { - const data = await request.json() as { id?: string } - if (!data.id) { - return json({ message: 'No ID provided' }, { - status: 400 - }) - } - - const session = await getSession(request.headers.get('Cookie')) - if (!session.has('hsApiKey')) { - return json({ message: 'Unauthorized' }, { - status: 401 - }) - } - - await del(`v1/node/${data.id}`, session.get('hsApiKey')!) - return json({ message: 'Machine removed' }) -} - -export default function Page() { - useLiveData({ interval: 3000 }) - const data = useLoaderData() - const fetcher = useFetcher() - - const { Modal, open } = useModal() - - return ( - <> - {Modal} - - - - - - - - - - {data.map(machine => { - const tags = [...machine.forcedTags, ...machine.validTags] - return ( - - - - - - - ) - })} - -
NameIP AddressesLast Seen
- -

{machine.givenName}

- - {machine.name} - -
- {tags.map(tag => ( - - {tag} - - ))} -
- -
- {machine.ipAddresses.map((ip, index) => ( - - ))} - - - -

- {machine.online - ? 'Connected' - : new Date( - machine.lastSeen - ).toLocaleString()} -

-
-
-
- - )} - > - - - - - - - - - - - - - -
-
- - ) -} diff --git a/app/routes/_data.machines._index/machine.tsx b/app/routes/_data.machines._index/machine.tsx new file mode 100644 index 0000000..daaa4ef --- /dev/null +++ b/app/routes/_data.machines._index/machine.tsx @@ -0,0 +1,204 @@ +import { ChevronDownIcon, ClipboardIcon, EllipsisHorizontalIcon } from '@heroicons/react/24/outline' +import { type FetcherWithComponents, Link } from '@remix-run/react' +import clsx from 'clsx' +import toast from 'react-hot-toast/headless' + +import Dropdown from '~/components/Dropdown' +import type { OpenFunction } from '~/components/Modal' +import StatusCircle from '~/components/StatusCircle' +import { type Machine } from '~/types' + +type MachineProperties = { + readonly machine: Machine; + readonly open: OpenFunction; + readonly fetcher: FetcherWithComponents; + readonly magic?: string; +} + +export default function MachineRow({ machine, open, fetcher, magic }: MachineProperties) { + const tags = [...machine.forcedTags, ...machine.validTags] + return ( + + + +

+ {machine.givenName} +

+

+ {machine.name} +

+
+ {tags.map(tag => ( + + {tag} + + ))} +
+ + + +
+ {machine.ipAddresses[0]} + + )} + > + {machine.ipAddresses.map(ip => ( + + + + ))} + {magic ? ( + + + + ) : undefined} + +
+ + + + +

+ {machine.online + ? 'Connected' + : new Date( + machine.lastSeen + ).toLocaleString()} +

+
+ + +
+ + )} + > + + + + + + + + + + + + + +
+ + + ) +} diff --git a/app/routes/_data.machines._index/route.tsx b/app/routes/_data.machines._index/route.tsx new file mode 100644 index 0000000..4862455 --- /dev/null +++ b/app/routes/_data.machines._index/route.tsx @@ -0,0 +1,121 @@ + +/* eslint-disable @typescript-eslint/no-non-null-assertion */ + +import { InformationCircleIcon } from '@heroicons/react/24/outline' +import { type ActionFunctionArgs, json, type LoaderFunctionArgs } from '@remix-run/node' +import { useFetcher, useLoaderData } from '@remix-run/react' +import clsx from 'clsx' +import { Button, Tooltip, TooltipTrigger } from 'react-aria-components' + +import Code from '~/components/Code' +import useModal from '~/components/Modal' +import { type Machine } from '~/types' +import { getConfig, getContext } from '~/utils/config' +import { del, pull } from '~/utils/headscale' +import { getSession } from '~/utils/sessions' +import { useLiveData } from '~/utils/useLiveData' + +import MachineRow from './machine' + +export async function loader({ request }: LoaderFunctionArgs) { + const session = await getSession(request.headers.get('Cookie')) + + const data = await pull<{ nodes: Machine[] }>('v1/node', session.get('hsApiKey')!) + const context = await getContext() + + let magic: string | undefined + if (context.hasConfig) { + const config = await getConfig() + if (config.dns_config.magic_dns) { + magic = config.dns_config.base_domain + } + } + + return { + nodes: data.nodes, + magic + } +} + +export async function action({ request }: ActionFunctionArgs) { + const data = await request.json() as { id?: string } + if (!data.id) { + return json({ message: 'No ID provided' }, { + status: 400 + }) + } + + const session = await getSession(request.headers.get('Cookie')) + if (!session.has('hsApiKey')) { + return json({ message: 'Unauthorized' }, { + status: 401 + }) + } + + await del(`v1/node/${data.id}`, session.get('hsApiKey')!) + return json({ message: 'Machine removed' }) +} + +export default function Page() { + useLiveData({ interval: 3000 }) + const data = useLoaderData() + const fetcher = useFetcher() + + const { Modal, open } = useModal() + + return ( + <> + {Modal} +

Machines

+ + + + + + + + + + {data.nodes.map(machine => ( + + ))} + +
Name +
+ Addresses + {data.magic ? ( + + + + Since MagicDNS is enabled, you can access devices + based on their name and also at + {' '} + + [name].[user].{data.magic} + + + + ) : undefined} +
+
Last Seen
+ + ) +} diff --git a/app/routes/_data.tsx b/app/routes/_data.tsx index 4f8b868..f94ae14 100644 --- a/app/routes/_data.tsx +++ b/app/routes/_data.tsx @@ -45,7 +45,7 @@ export default function Layout() { const data = useLoaderData() return ( <> -
+