feat: switch agent fetching to the server side
this brings the benefit of fitting in the revalidator lifecycle we have created via the useLiveData hook.
This commit is contained in:
parent
9a1051b9af
commit
73ea35980d
@ -53,5 +53,11 @@ func httpToWs(controlURL string) (string, error) {
|
||||
return "", fmt.Errorf("unsupported scheme: %s", u.Scheme)
|
||||
}
|
||||
|
||||
// We also need to append /_dial to the path
|
||||
if u.Path[len(u.Path)-1] != '/' {
|
||||
u.Path += "/"
|
||||
}
|
||||
|
||||
u.Path += "_dial"
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
@ -11,9 +11,6 @@ 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', [
|
||||
|
||||
@ -1,39 +0,0 @@
|
||||
import { LoaderFunctionArgs } from 'react-router';
|
||||
import { hp_getSingleton, hp_getSingletonUnsafe } from '~server/context/global';
|
||||
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const data = hp_getSingletonUnsafe('ws_agent_data');
|
||||
|
||||
if (!data) {
|
||||
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 = data.toJSON();
|
||||
const missing = nodeIds.filter((nodeID) => !entries[nodeID]);
|
||||
if (missing.length > 0) {
|
||||
const requestCall = hp_getSingleton('ws_fetch_data');
|
||||
requestCall(missing);
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify(data), {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -8,7 +8,6 @@ import Tooltip from '~/components/Tooltip';
|
||||
import type { LoadContext } from '~/server';
|
||||
import type { Machine, Route, User } from '~/types';
|
||||
import cn from '~/utils/cn';
|
||||
import useAgent from '~/utils/use-agent';
|
||||
import { menuAction } from './action';
|
||||
import MachineRow from './components/machine';
|
||||
import NewMachine from './dialogs/new';
|
||||
@ -45,6 +44,7 @@ export async function loader({
|
||||
server: context.config.headscale.url,
|
||||
publicServer: context.config.headscale.public_url,
|
||||
agents: context.agents?.tailnetIDs(),
|
||||
stats: context.agents?.lookup(machines.nodes.map((node) => node.nodeKey)),
|
||||
};
|
||||
}
|
||||
|
||||
@ -54,7 +54,6 @@ 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 (
|
||||
<>
|
||||
@ -120,10 +119,10 @@ export default function Page() {
|
||||
)}
|
||||
users={data.users}
|
||||
magic={data.magic}
|
||||
stats={stats?.[machine.nodeKey]}
|
||||
// If we pass undefined, the column will not be rendered
|
||||
// This is useful for when there are no agents configured
|
||||
isAgent={data.agents?.includes(machine.id)}
|
||||
stats={data.stats?.[machine.nodeKey]}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
@ -74,11 +74,17 @@ export default await createHonoServer({
|
||||
return appLoadContext;
|
||||
},
|
||||
|
||||
configure(server, { upgradeWebSocket }) {
|
||||
if (appLoadContext.agents !== undefined) {
|
||||
// We need this since we cannot pass the WSEvents context
|
||||
(upgradeWebSocket as UpgradeWebSocket<WebSocket>)(
|
||||
appLoadContext.agents.configureSocket,
|
||||
configure(app, { upgradeWebSocket }) {
|
||||
const agentManager = appLoadContext.agents;
|
||||
if (agentManager) {
|
||||
app.get(
|
||||
`${__PREFIX__}/_dial`,
|
||||
// We need this since we cannot pass the WSEvents context
|
||||
// Also important to not pass the callback directly
|
||||
// since we need to retain `this` context
|
||||
(upgradeWebSocket as UpgradeWebSocket<WebSocket>)((c) =>
|
||||
agentManager.configureSocket(c),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@ -49,10 +49,20 @@ class AgentManager {
|
||||
return Array.from(this.agents.keys());
|
||||
}
|
||||
|
||||
lookup(nodeIds: string[]) {
|
||||
const entries = this.cache.toJSON();
|
||||
const missing = nodeIds.filter((nodeId) => !entries[nodeId]);
|
||||
if (missing.length > 0) {
|
||||
this.requestData(missing);
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
// Request data from all connected agents
|
||||
// This does not return anything, but caches the data which then needs to be
|
||||
// queried by the caller separately.
|
||||
requestData(nodeList: string[]) {
|
||||
private requestData(nodeList: string[]) {
|
||||
const NodeIDs = [...new Set(nodeList)];
|
||||
NodeIDs.map((node) => {
|
||||
log.debug('agent', 'Requesting agent data for %s', node);
|
||||
|
||||
@ -1,24 +0,0 @@
|
||||
import { useEffect, useMemo, useRef } from 'react';
|
||||
import { useFetcher } from 'react-router';
|
||||
import { HostInfo } from '~/types';
|
||||
|
||||
export default function useAgent(nodeIds: string[]) {
|
||||
const fetcher = useFetcher<Record<string, HostInfo>>();
|
||||
const qp = useMemo(
|
||||
() => new URLSearchParams({ node_ids: nodeIds.join(',') }),
|
||||
[nodeIds],
|
||||
);
|
||||
|
||||
const idRef = useRef<string[]>([]);
|
||||
useEffect(() => {
|
||||
if (idRef.current.join(',') !== nodeIds.join(',')) {
|
||||
fetcher.load(`/api/agent?${qp.toString()}`);
|
||||
idRef.current = nodeIds;
|
||||
}
|
||||
}, [qp.toString()]);
|
||||
|
||||
return {
|
||||
data: fetcher.data,
|
||||
isLoading: fetcher.state === 'loading',
|
||||
};
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user