From 13dc131cfc109f67e83dbb097619f476a90eeceb Mon Sep 17 00:00:00 2001 From: Pulse Monitor Date: Wed, 10 Sep 2025 13:30:55 +0000 Subject: [PATCH] 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. --- .../src/components/Dashboard/Dashboard.tsx | 18 ++++++++++- .../src/components/Dashboard/GuestRow.tsx | 22 ++----------- frontend-modern/vite.config.dev.ts | 31 ------------------- 3 files changed, 19 insertions(+), 52 deletions(-) delete mode 100644 frontend-modern/vite.config.dev.ts diff --git a/frontend-modern/src/components/Dashboard/Dashboard.tsx b/frontend-modern/src/components/Dashboard/Dashboard.tsx index 2c7dd1cf3..02aad88ca 100644 --- a/frontend-modern/src/components/Dashboard/Dashboard.tsx +++ b/frontend-modern/src/components/Dashboard/Dashboard.tsx @@ -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(null); + const [guestMetadata, setGuestMetadata] = createSignal>({}); // 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) { {(() => { const guestId = guest.id || `${guest.instance}-${guest.name}-${guest.vmid}`; + const metadata = guestMetadata()[guestId] || guestMetadata()[`${guest.node}-${guest.vmid}`]; return ( diff --git a/frontend-modern/src/components/Dashboard/GuestRow.tsx b/frontend-modern/src/components/Dashboard/GuestRow.tsx index 289f63b8d..54b81f250 100644 --- a/frontend-modern/src/components/Dashboard/GuestRow.tsx +++ b/frontend-modern/src/components/Dashboard/GuestRow.tsx @@ -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); diff --git a/frontend-modern/vite.config.dev.ts b/frontend-modern/vite.config.dev.ts deleted file mode 100644 index f697aeb4b..000000000 --- a/frontend-modern/vite.config.dev.ts +++ /dev/null @@ -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', - }, -});