feat: update more primitives to use the new api

This commit is contained in:
Aarnav Tale
2025-11-04 23:06:56 -05:00
parent d68737e410
commit c84e9ca4a8
9 changed files with 109 additions and 116 deletions
+4 -4
View File
@@ -1,6 +1,6 @@
import { CheckCircle, CircleSlash, Info, UserCircle } from 'lucide-react';
import { useMemo, useState } from 'react';
import { data, Link as RemixLink, useLoaderData } from 'react-router';
import { data, Link as RemixLink } from 'react-router';
import Attribute from '~/components/Attribute';
import Button from '~/components/Button';
import Card from '~/components/Card';
@@ -58,9 +58,9 @@ export async function loader({ request, params, context }: Route.LoaderArgs) {
export const action = machineAction;
export default function Page() {
const { node, tags, magic, users, agent, stats } =
useLoaderData<typeof loader>();
export default function Page({
loaderData: { node, tags, users, magic, agent, stats },
}: Route.ComponentProps) {
const [showRouting, setShowRouting] = useState(false);
const uiTags = useMemo(() => {
+21 -21
View File
@@ -1,5 +1,4 @@
import { Info } from 'lucide-react';
import { useLoaderData } from 'react-router';
import Code from '~/components/Code';
import Link from '~/components/Link';
import Tooltip from '~/components/Tooltip';
@@ -67,9 +66,7 @@ export async function loader({ request, context }: Route.LoaderArgs) {
export const action = machineAction;
export default function Page() {
const data = useLoaderData<typeof loader>();
export default function Page({ loaderData }: Route.ComponentProps) {
return (
<>
<div className="flex justify-between items-center mb-6">
@@ -86,10 +83,10 @@ export default function Page() {
</p>
</div>
<NewMachine
disabledKeys={data.preAuth ? [] : ['pre-auth']}
isDisabled={!data.writable}
server={data.publicServer ?? data.server}
users={data.users}
disabledKeys={loaderData.preAuth ? [] : ['pre-auth']}
isDisabled={!loaderData.writable}
server={loaderData.publicServer ?? loaderData.server}
users={loaderData.users}
/>
</div>
<table className="table-auto w-full rounded-lg">
@@ -99,7 +96,7 @@ export default function Page() {
<th className="pb-2 w-1/4">
<div className="flex items-center gap-x-1">
<p className="uppercase text-xs font-bold">Addresses</p>
{data.magic ? (
{loaderData.magic ? (
<Tooltip>
<Info className="w-4 h-4" />
<Tooltip.Body className="font-normal">
@@ -107,7 +104,7 @@ export default function Page() {
their name and also at{' '}
<Code>
[name].
{data.magic}
{loaderData.magic}
</Code>
</Tooltip.Body>
</Tooltip>
@@ -115,7 +112,7 @@ export default function Page() {
</div>
</th>
{/* We only want to show the version column if there are agents */}
{data.agent !== undefined ? (
{loaderData.agent !== undefined ? (
<th className="uppercase text-xs font-bold pb-2">Version</th>
) : undefined}
<th className="uppercase text-xs font-bold pb-2">Last Seen</th>
@@ -127,18 +124,21 @@ export default function Page() {
'border-t border-headplane-100 dark:border-headplane-800',
)}
>
{data.populatedNodes.map((machine) => (
{loaderData.populatedNodes.map((node) => (
<MachineRow
isAgent={data.agent ? data.agent === machine.nodeKey : undefined}
isDisabled={
data.writable
? false // If the user has write permissions, they can edit all machines
: machine.user.providerId?.split('/').pop() !== data.subject
isAgent={
loaderData.agent ? loaderData.agent === node.nodeKey : undefined
}
key={machine.id}
magic={data.magic}
node={machine}
users={data.users}
isDisabled={
loaderData.writable
? false // If the user has write permissions, they can edit all machines
: node.user.providerId?.split('/').pop() !==
loaderData.subject
}
key={node.id}
magic={loaderData.magic}
node={node}
users={loaderData.users}
/>
))}
</tbody>
+20 -43
View File
@@ -4,20 +4,11 @@ import { faker } from '@faker-js/faker';
import { eq } from 'drizzle-orm';
import { Loader2 } from 'lucide-react';
import { useEffect, useState } from 'react';
import {
ActionFunctionArgs,
data,
LinksFunction,
LoaderFunctionArgs,
ShouldRevalidateFunction,
useLoaderData,
useSubmit,
} from 'react-router';
import { data, type ShouldRevalidateFunction, useSubmit } from 'react-router';
import { ExternalScriptsHandle } from 'remix-utils/external-scripts';
import { LoadContext } from '~/server';
import { EphemeralNodeInsert, ephemeralNodes } from '~/server/db/schema';
import { Machine, PreAuthKey, User } from '~/types';
import { useLiveData } from '~/utils/live-data';
import type { Route } from './+types/console';
import UserPrompt from './user-prompt';
import XTerm from './xterm.client';
@@ -25,10 +16,7 @@ export const shouldRevalidate: ShouldRevalidateFunction = () => {
return false;
};
export async function loader({
request,
context,
}: LoaderFunctionArgs<LoadContext>) {
export async function loader({ request, context }: Route.LoaderArgs) {
const origin = new URL(request.url).origin;
const assets = ['/wasm_exec.js', '/hp_ssh.wasm'];
const missing: string[] = [];
@@ -53,10 +41,9 @@ export async function loader({
if (session.user.subject === 'unknown-non-oauth') {
throw data('Only OAuth users are allowed to use WebSSH', 403);
}
const { users } = await context.client.get<{ users: User[] }>(
'v1/user',
session.api_key,
);
const api = context.hsApi.getRuntimeClient(session.api_key);
const users = await api.getUsers();
// MARK: This assumes that a user has authenticated with Headscale first
// Since the only way to enforce permissions via ACLs is to generate a
@@ -77,15 +64,12 @@ export async function loader({
);
}
const { preAuthKey } = await context.client.post<{ preAuthKey: PreAuthKey }>(
'v1/preauthkey',
session.api_key,
{
user: lookup.id,
reusable: false,
ephemeral: true,
expiration: new Date(Date.now() + 60 * 1000).toISOString(), // 1 minute
},
const preAuthKey = await api.createPreAuthKey(
lookup.id,
true, // ephemeral
false, // reusable
new Date(Date.now() + 60 * 1000), // expiration: 1 minute
null, // aclTags
);
// TODO: Enable config to enforce generate_authkeys capability
@@ -130,13 +114,8 @@ export async function loader({
// );
// }
const { nodes } = await context.client.get<{ nodes: Machine[] }>(
'v1/node',
session.api_key,
);
// node.name is the hostname, given_name is the set name
const lookupNode = nodes.find((n) => n.name === hostname);
const nodes = await api.getNodes();
const lookupNode = nodes.find((n) => n.givenName === hostname);
if (!lookupNode) {
throw data(`Node with hostname ${hostname} not found`, 404);
}
@@ -182,11 +161,8 @@ function generateHostname(username: string) {
return `ssh-${adjective}-${noun}-${username}`;
}
export async function action({
request,
context,
}: ActionFunctionArgs<LoadContext>) {
const _session = await context.sessions.auth(request);
export async function action({ request, context }: Route.ActionArgs) {
await context.sessions.auth(request);
if (!context.agents?.agentID()) {
throw data(
'WebSSH is only available with the Headplane agent integration',
@@ -214,7 +190,7 @@ export async function action({
.where(eq(ephemeralNodes.auth_key, authKey));
}
export const links: LinksFunction = () => [
export const links: Route.LinksFunction = () => [
{
rel: 'preload',
href: '/hp_ssh.wasm',
@@ -234,13 +210,14 @@ export const handle: ExternalScriptsHandle = {
],
};
export default function Page() {
export default function Page({
loaderData: { ipnDetails, sshDetails },
}: Route.ComponentProps) {
const submit = useSubmit();
const { pause } = useLiveData();
const [ipn, setIpn] = useState<TsWasmNet | null>(null);
const [nodeKey, setNodeKey] = useState<string | null>(null);
const { ipnDetails, sshDetails } = useLoaderData<typeof loader>();
useEffect(() => {
if (!ipnDetails) {
+6 -4
View File
@@ -1,8 +1,10 @@
import { LoaderFunctionArgs } from 'react-router';
import type { LoadContext } from '~/server';
import type { Route } from './+types/healthz';
export async function loader({ context }: Route.LoaderArgs) {
// Use a fake API key for healthcheck
const api = context.hsApi.getRuntimeClient('fake-api-key');
const healthy = await api.isHealthy();
export async function loader({ context }: LoaderFunctionArgs<LoadContext>) {
const healthy = await context.client.healthcheck();
return new Response(JSON.stringify({ status: healthy ? 'OK' : 'ERROR' }), {
status: healthy ? 200 : 500,
headers: {