perf: load all guest metadata in single API call (addresses #398)

Instead of making individual API calls for each guest's metadata,
load all metadata once at the Dashboard level and pass it down as props.
This reduces hundreds of HTTP requests to just one when dealing with
large deployments.

With 800 guests, this changes from 800 individual requests to 1 batch request.
This commit is contained in:
Pulse Monitor 2025-09-10 13:30:55 +00:00
parent 6491c2da14
commit 13dc131cfc
3 changed files with 19 additions and 52 deletions

View file

@ -1,4 +1,4 @@
import { createSignal, createMemo, createEffect, For, Show } from 'solid-js';
import { createSignal, createMemo, createEffect, For, Show, onMount } from 'solid-js';
import type { VM, Container, Node } from '@/types/api';
import { GuestRow } from './GuestRow';
import { useWebSocket } from '@/App';
@ -11,6 +11,8 @@ import { UnifiedNodeSelector } from '@/components/shared/UnifiedNodeSelector';
import { MetricBar } from './MetricBar';
import { formatBytes, formatUptime } from '@/utils/format';
import { DashboardFilter } from './DashboardFilter';
import { GuestMetadataAPI } from '@/api/guestMetadata';
import type { GuestMetadata } from '@/api/guestMetadata';
interface DashboardProps {
vms: VM[];
@ -28,6 +30,7 @@ export function Dashboard(props: DashboardProps) {
const [search, setSearch] = createSignal('');
const [isSearchLocked, setIsSearchLocked] = createSignal(false);
const [selectedNode, setSelectedNode] = createSignal<string | null>(null);
const [guestMetadata, setGuestMetadata] = createSignal<Record<string, GuestMetadata>>({});
// Initialize from localStorage with proper type checking
const storedViewMode = localStorage.getItem('dashboardViewMode');
@ -78,6 +81,17 @@ export function Dashboard(props: DashboardProps) {
// Create tooltip system
const TooltipComponent = createTooltipSystem();
// Load all guest metadata on mount (single API call for all guests)
onMount(async () => {
try {
const metadata = await GuestMetadataAPI.getAllMetadata();
setGuestMetadata(metadata || {});
} catch (err) {
// Silently fail - metadata is optional for display
console.debug('Failed to load guest metadata:', err);
}
});
// Create a mapping from node name to host URL
@ -780,10 +794,12 @@ export function Dashboard(props: DashboardProps) {
<ComponentErrorBoundary name="GuestRow">
{(() => {
const guestId = guest.id || `${guest.instance}-${guest.name}-${guest.vmid}`;
const metadata = guestMetadata()[guestId] || guestMetadata()[`${guest.node}-${guest.vmid}`];
return (
<GuestRow
guest={guest}
alertStyles={getAlertStyles(guestId, activeAlerts)}
customUrl={metadata?.customUrl}
onTagClick={handleTagClick}
activeSearch={search()}
/>

View file

@ -1,10 +1,9 @@
import { Show, createMemo, createSignal, createEffect, onMount } from 'solid-js';
import { Show, createMemo, createSignal, createEffect } from 'solid-js';
import type { VM, Container } from '@/types/api';
import { formatBytes, formatUptime } from '@/utils/format';
import { MetricBar } from './MetricBar';
import { IOMetric } from './IOMetric';
import { TagBadges } from './TagBadges';
import { GuestMetadataAPI } from '@/api/guestMetadata';
type Guest = VM | Container;
@ -39,24 +38,7 @@ export function GuestRow(props: GuestRowProps) {
// Update custom URL when prop changes
createEffect(() => {
if (props.customUrl !== undefined) {
setCustomUrl(props.customUrl);
}
});
// Load custom URL from backend if not provided via props
onMount(async () => {
if (!props.customUrl) {
try {
const metadata = await GuestMetadataAPI.getMetadata(guestId());
if (metadata && metadata.customUrl) {
setCustomUrl(metadata.customUrl);
}
} catch (err) {
// Silently fail - not critical for display
console.debug('Failed to load guest metadata:', err);
}
}
setCustomUrl(props.customUrl);
});
const cpuPercent = createMemo(() => (props.guest.cpu || 0) * 100);

View file

@ -1,31 +0,0 @@
import { defineConfig } from 'vite';
import solid from 'vite-plugin-solid';
import path from 'path';
export default defineConfig({
plugins: [solid()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
server: {
port: 7655,
host: '0.0.0.0',
strictPort: true, // FAIL if port 7655 is not available
proxy: {
'/api': {
target: 'http://127.0.0.1:7656',
changeOrigin: true,
},
'/ws': {
target: 'ws://127.0.0.1:7656',
ws: true,
changeOrigin: true,
},
},
},
build: {
target: 'esnext',
},
});