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)
|
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
|
return u.String(), nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,9 +11,6 @@ export default [
|
|||||||
route('/oidc/callback', 'routes/auth/oidc-callback.ts'),
|
route('/oidc/callback', 'routes/auth/oidc-callback.ts'),
|
||||||
route('/oidc/start', 'routes/auth/oidc-start.ts'),
|
route('/oidc/start', 'routes/auth/oidc-start.ts'),
|
||||||
|
|
||||||
// API
|
|
||||||
route('/api/agent', 'routes/api/agent.ts'),
|
|
||||||
|
|
||||||
// All the main logged-in dashboard routes
|
// All the main logged-in dashboard routes
|
||||||
// Double nested to separate error propagations
|
// Double nested to separate error propagations
|
||||||
layout('layouts/shell.tsx', [
|
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 { LoadContext } from '~/server';
|
||||||
import type { Machine, Route, User } from '~/types';
|
import type { Machine, Route, User } from '~/types';
|
||||||
import cn from '~/utils/cn';
|
import cn from '~/utils/cn';
|
||||||
import useAgent from '~/utils/use-agent';
|
|
||||||
import { menuAction } from './action';
|
import { menuAction } from './action';
|
||||||
import MachineRow from './components/machine';
|
import MachineRow from './components/machine';
|
||||||
import NewMachine from './dialogs/new';
|
import NewMachine from './dialogs/new';
|
||||||
@ -45,6 +44,7 @@ export async function loader({
|
|||||||
server: context.config.headscale.url,
|
server: context.config.headscale.url,
|
||||||
publicServer: context.config.headscale.public_url,
|
publicServer: context.config.headscale.public_url,
|
||||||
agents: context.agents?.tailnetIDs(),
|
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() {
|
export default function Page() {
|
||||||
const data = useLoaderData<typeof loader>();
|
const data = useLoaderData<typeof loader>();
|
||||||
const { data: stats } = useAgent(data.nodes.map((node) => node.nodeKey));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -120,10 +119,10 @@ export default function Page() {
|
|||||||
)}
|
)}
|
||||||
users={data.users}
|
users={data.users}
|
||||||
magic={data.magic}
|
magic={data.magic}
|
||||||
stats={stats?.[machine.nodeKey]}
|
|
||||||
// If we pass undefined, the column will not be rendered
|
// If we pass undefined, the column will not be rendered
|
||||||
// This is useful for when there are no agents configured
|
// This is useful for when there are no agents configured
|
||||||
isAgent={data.agents?.includes(machine.id)}
|
isAgent={data.agents?.includes(machine.id)}
|
||||||
|
stats={data.stats?.[machine.nodeKey]}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@ -74,11 +74,17 @@ export default await createHonoServer({
|
|||||||
return appLoadContext;
|
return appLoadContext;
|
||||||
},
|
},
|
||||||
|
|
||||||
configure(server, { upgradeWebSocket }) {
|
configure(app, { upgradeWebSocket }) {
|
||||||
if (appLoadContext.agents !== undefined) {
|
const agentManager = appLoadContext.agents;
|
||||||
|
if (agentManager) {
|
||||||
|
app.get(
|
||||||
|
`${__PREFIX__}/_dial`,
|
||||||
// We need this since we cannot pass the WSEvents context
|
// We need this since we cannot pass the WSEvents context
|
||||||
(upgradeWebSocket as UpgradeWebSocket<WebSocket>)(
|
// Also important to not pass the callback directly
|
||||||
appLoadContext.agents.configureSocket,
|
// 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());
|
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
|
// Request data from all connected agents
|
||||||
// This does not return anything, but caches the data which then needs to be
|
// This does not return anything, but caches the data which then needs to be
|
||||||
// queried by the caller separately.
|
// queried by the caller separately.
|
||||||
requestData(nodeList: string[]) {
|
private requestData(nodeList: string[]) {
|
||||||
const NodeIDs = [...new Set(nodeList)];
|
const NodeIDs = [...new Set(nodeList)];
|
||||||
NodeIDs.map((node) => {
|
NodeIDs.map((node) => {
|
||||||
log.debug('agent', 'Requesting agent data for %s', 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