feat: use a new ws implementation thats encapsulated

This commit is contained in:
Aarnav Tale
2025-03-06 17:32:33 -05:00
parent 45537620a6
commit 5dd4c41291
13 changed files with 370 additions and 101 deletions
+3
View File
@@ -11,6 +11,9 @@ export default [
route('/oidc/callback', 'routes/auth/oidc-callback.ts'),
route('/oidc/start', 'routes/auth/oidc-start.ts'),
// API
route('/api/agent', 'routes/api/agent.ts'),
// All the main logged-in dashboard routes
// Double nested to separate error propagations
layout('layouts/shell.tsx', [
+39
View File
@@ -0,0 +1,39 @@
import { LoaderFunctionArgs } from 'react-router';
import type { AppContext } from '~server/context/app';
export async function loader({
request,
context,
}: LoaderFunctionArgs<AppContext>) {
if (!context?.agentData) {
return new Response(JSON.stringify({ error: 'Agent data unavailable' }), {
status: 400,
headers: {
'Content-Type': 'application/json',
},
});
}
const qp = new URLSearchParams(request.url.split('?')[1]);
const nodeIds = qp.get('node_ids')?.split(',');
if (!nodeIds) {
return new Response(JSON.stringify({ error: 'No node IDs provided' }), {
status: 400,
headers: {
'Content-Type': 'application/json',
},
});
}
const entries = context.agentData.toJSON();
const missing = nodeIds.filter((nodeID) => !entries[nodeID]);
if (missing.length > 0) {
await context.hp_agentRequest(missing);
}
return new Response(JSON.stringify(context.agentData), {
headers: {
'Content-Type': 'application/json',
},
});
}
+7 -13
View File
@@ -146,24 +146,18 @@ export default function MachineRow({
</Menu>
</div>
</td>
{/**
<td className="py-2">
{stats !== undefined ? (
<>
<p className="leading-snug">
{hinfo.getTSVersion(stats)}
</p>
{stats !== undefined ? (
<>
<p className="leading-snug">{hinfo.getTSVersion(stats)}</p>
<p className="text-sm opacity-50 max-w-48 truncate">
{hinfo.getOSInfo(stats)}
</p>
</>
) : (
<p className="text-sm opacity-50">
Unknown
</p>
)}
</>
) : (
<p className="text-sm opacity-50">Unknown</p>
)}
</td>
**/}
<td className="py-2">
<span
className={cn(
+11 -9
View File
@@ -148,16 +148,18 @@ export default function Page() {
{machine.user.name}
</div>
</div>
<div className="p-2 pl-4">
<p className="text-sm text-headplane-600 dark:text-headplane-300">
Status
</p>
<div className="flex gap-1 mt-1 mb-8">
{tags.map((tag) => (
<Chip key={tag} text={tag} />
))}
{tags.length > 0 ? (
<div className="p-2 pl-4">
<p className="text-sm text-headplane-600 dark:text-headplane-300">
Status
</p>
<div className="flex gap-1 mt-1 mb-8">
{tags.map((tag) => (
<Chip key={tag} text={tag} />
))}
</div>
</div>
</div>
) : undefined}
</div>
<h2 className="text-xl font-medium mb-4 mt-8">Subnets & Routing</h2>
<Routes
+4 -8
View File
@@ -9,11 +9,11 @@ import type { Machine, Route, User } from '~/types';
import cn from '~/utils/cn';
import { pull } from '~/utils/headscale';
import { getSession } from '~/utils/sessions.server';
import { initAgentSocket, queryAgent } from '~/utils/ws-agent';
import Tooltip from '~/components/Tooltip';
import { hs_getConfig } from '~/utils/config/loader';
import { noContext } from '~/utils/log';
import useAgent from '~/utils/useAgent';
import { AppContext } from '~server/context/app';
import { menuAction } from './action';
import MachineRow from './components/machine';
@@ -34,12 +34,8 @@ export async function loader({
throw noContext();
}
initAgentSocket(context);
const stats = await queryAgent(machines.nodes.map((node) => node.nodeKey));
const ctx = context.context;
const { mode, config } = hs_getConfig();
let magic: string | undefined;
if (mode !== 'no') {
@@ -53,7 +49,6 @@ export async function loader({
routes: routes.routes,
users: users.users,
magic,
stats,
server: ctx.headscale.url,
publicServer: ctx.headscale.public_url,
};
@@ -65,6 +60,7 @@ export async function action({ request }: ActionFunctionArgs) {
export default function Page() {
const data = useLoaderData<typeof loader>();
const { data: stats } = useAgent(data.nodes.map((node) => node.nodeKey));
return (
<>
@@ -108,7 +104,7 @@ export default function Page() {
) : undefined}
</div>
</th>
{/**<th className="uppercase text-xs font-bold pb-2">Version</th>**/}
<th className="uppercase text-xs font-bold pb-2">Version</th>
<th className="uppercase text-xs font-bold pb-2">Last Seen</th>
</tr>
</thead>
@@ -127,7 +123,7 @@ export default function Page() {
)}
users={data.users}
magic={data.magic}
stats={data.stats?.[machine.nodeKey]}
stats={stats?.[machine.nodeKey]}
/>
))}
</tbody>
+25
View File
@@ -0,0 +1,25 @@
import { useEffect } from 'react';
import { useFetcher } from 'react-router';
import { HostInfo } from '~/types';
export default function useAgent(nodeIds: string[], interval = 3000) {
const fetcher = useFetcher<Record<string, HostInfo>>();
useEffect(() => {
const qp = new URLSearchParams({ node_ids: nodeIds.join(',') });
fetcher.load(`/api/agent?${qp.toString()}`);
const intervalID = setInterval(() => {
fetcher.load(`/api/agent?${qp.toString()}`);
}, interval);
return () => {
clearInterval(intervalID);
};
}, [fetcher, interval, nodeIds]);
return {
data: fetcher.data,
isLoading: fetcher.state === 'loading',
};
}