fix(frontend): defer discovery tab initialization until opened
Some checks failed
Build and Test / Secret Scan (push) Has been cancelled
Build and Test / Frontend & Backend (push) Has been cancelled
Core E2E Tests / Playwright Core E2E (push) Has been cancelled

This commit is contained in:
rcourtman 2026-03-10 23:14:30 +00:00
parent d05a00b931
commit fde4d9124e
5 changed files with 689 additions and 245 deletions

View file

@ -0,0 +1,65 @@
import { cleanup, fireEvent, render, screen } from '@solidjs/testing-library';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { GuestDrawer } from './GuestDrawer';
vi.mock('../Discovery/DiscoveryTab', () => ({
DiscoveryTab: () => <div data-testid="discovery-tab">Discovery content</div>,
}));
vi.mock('./DiskList', () => ({
DiskList: () => <div data-testid="disk-list" />,
}));
vi.mock('../shared/HistoryChart', () => ({
HistoryChart: () => <div data-testid="history-chart" />,
}));
vi.mock('@/stores/license', () => ({
hasFeature: () => true,
}));
describe('GuestDrawer discovery activation', () => {
afterEach(() => {
cleanup();
});
it('does not mount DiscoveryTab until the discovery tab is opened', async () => {
render(() => (
<GuestDrawer
guest={
{
id: 'pve1:node1:100',
instance: 'pve1',
node: 'node1',
vmid: 100,
name: 'vm100',
type: 'qemu',
status: 'running',
cpus: 2,
uptime: 3600,
memory: {
total: 8 * 1024 * 1024 * 1024,
used: 2 * 1024 * 1024 * 1024,
free: 6 * 1024 * 1024 * 1024,
usage: 25,
},
disks: [],
networkInterfaces: [],
ipAddresses: [],
osName: 'Ubuntu',
osVersion: '24.04',
agentVersion: '1.0',
} as any
}
metricsKey="vm:pve1:node1:100"
onClose={() => undefined}
/>
));
expect(screen.queryByTestId('discovery-tab')).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: 'Discovery' }));
expect(await screen.findByTestId('discovery-tab')).toBeInTheDocument();
});
});

View file

@ -107,12 +107,16 @@ export const GuestDrawer: Component<GuestDrawerProps> = (props) => {
}; };
const [activeTab, setActiveTab] = createSignal<'overview' | 'discovery'>('overview'); const [activeTab, setActiveTab] = createSignal<'overview' | 'discovery'>('overview');
const [discoveryActivated, setDiscoveryActivated] = createSignal(false);
// All tabs are always rendered (hidden via CSS) to avoid any DOM // All tabs are always rendered (hidden via CSS) to avoid any DOM
// mount/unmount during tab switches. Mounting new components inside // mount/unmount during tab switches. Mounting new components inside
// a <For>-rendered table row causes SolidJS to recreate the row, // a <For>-rendered table row causes SolidJS to recreate the row,
// which detaches the element and resets the scroll container. // which detaches the element and resets the scroll container.
const switchTab = (tab: 'overview' | 'discovery') => { const switchTab = (tab: 'overview' | 'discovery') => {
if (tab === 'discovery') {
setDiscoveryActivated(true);
}
setActiveTab(tab); setActiveTab(tab);
}; };
@ -558,13 +562,13 @@ export const GuestDrawer: Component<GuestDrawerProps> = (props) => {
</div> </div>
</div> </div>
{/* Always rendered, hidden via CSS. Wrapped in a local Suspense {/* Defer discovery initialization until the tab is first opened, then keep it
so DiscoveryTab's createResource loading state doesn't bubble mounted so subsequent tab switches don't churn the row. */}
up to the app-level Suspense and replace the entire page. */}
<div <div
class={activeTab() === 'discovery' ? '' : 'hidden'} class={activeTab() === 'discovery' ? '' : 'hidden'}
style={{ 'overflow-anchor': 'none' }} style={{ 'overflow-anchor': 'none' }}
> >
<Show when={discoveryActivated()}>
<Suspense <Suspense
fallback={ fallback={
<div class="flex items-center justify-center py-8"> <div class="flex items-center justify-center py-8">
@ -585,6 +589,7 @@ export const GuestDrawer: Component<GuestDrawerProps> = (props) => {
onCustomUrlChange={(url) => props.onCustomUrlChange?.(guestId(), url)} onCustomUrlChange={(url) => props.onCustomUrlChange?.(guestId(), url)}
/> />
</Suspense> </Suspense>
</Show>
</div> </div>
</div> </div>
); );

View file

@ -17,6 +17,7 @@ interface HostDrawerProps {
export const HostDrawer: Component<HostDrawerProps> = (props) => { export const HostDrawer: Component<HostDrawerProps> = (props) => {
const [activeTab, setActiveTab] = createSignal<'overview' | 'discovery'>('overview'); const [activeTab, setActiveTab] = createSignal<'overview' | 'discovery'>('overview');
const [discoveryActivated, setDiscoveryActivated] = createSignal(false);
const [historyRange, setHistoryRange] = useDrawerHistoryRange(`host:${props.host.id}`); const [historyRange, setHistoryRange] = useDrawerHistoryRange(`host:${props.host.id}`);
const [editingUrl, setEditingUrl] = createSignal(false); const [editingUrl, setEditingUrl] = createSignal(false);
const [urlInput, setUrlInput] = createSignal(''); const [urlInput, setUrlInput] = createSignal('');
@ -25,6 +26,9 @@ export const HostDrawer: Component<HostDrawerProps> = (props) => {
const metricsResource = { type: 'host' as ResourceType, id: props.host.id }; const metricsResource = { type: 'host' as ResourceType, id: props.host.id };
const switchTab = (tab: 'overview' | 'discovery') => { const switchTab = (tab: 'overview' | 'discovery') => {
if (tab === 'discovery') {
setDiscoveryActivated(true);
}
setActiveTab(tab); setActiveTab(tab);
}; };
@ -529,6 +533,7 @@ export const HostDrawer: Component<HostDrawerProps> = (props) => {
class={activeTab() === 'discovery' ? '' : 'hidden'} class={activeTab() === 'discovery' ? '' : 'hidden'}
style={{ 'overflow-anchor': 'none' }} style={{ 'overflow-anchor': 'none' }}
> >
<Show when={discoveryActivated()}>
<Suspense <Suspense
fallback={ fallback={
<div class="flex items-center justify-center py-8"> <div class="flex items-center justify-center py-8">
@ -550,6 +555,7 @@ export const HostDrawer: Component<HostDrawerProps> = (props) => {
commandsEnabled={props.host.commandsEnabled} commandsEnabled={props.host.commandsEnabled}
/> />
</Suspense> </Suspense>
</Show>
</div> </div>
</div> </div>
); );

View file

@ -17,6 +17,7 @@ interface NodeDrawerProps {
export const NodeDrawer: Component<NodeDrawerProps> = (props) => { export const NodeDrawer: Component<NodeDrawerProps> = (props) => {
const [activeTab, setActiveTab] = createSignal<'overview' | 'discovery'>('overview'); const [activeTab, setActiveTab] = createSignal<'overview' | 'discovery'>('overview');
const [discoveryActivated, setDiscoveryActivated] = createSignal(false);
const [historyRange, setHistoryRange] = useDrawerHistoryRange( const [historyRange, setHistoryRange] = useDrawerHistoryRange(
`node:${props.node.id || props.node.name}`, `node:${props.node.id || props.node.name}`,
); );
@ -49,6 +50,9 @@ export const NodeDrawer: Component<NodeDrawerProps> = (props) => {
}; };
const switchTab = (tab: 'overview' | 'discovery') => { const switchTab = (tab: 'overview' | 'discovery') => {
if (tab === 'discovery') {
setDiscoveryActivated(true);
}
setActiveTab(tab); setActiveTab(tab);
}; };
@ -327,6 +331,7 @@ export const NodeDrawer: Component<NodeDrawerProps> = (props) => {
class={activeTab() === 'discovery' ? '' : 'hidden'} class={activeTab() === 'discovery' ? '' : 'hidden'}
style={{ 'overflow-anchor': 'none' }} style={{ 'overflow-anchor': 'none' }}
> >
<Show when={discoveryActivated()}>
<Suspense <Suspense
fallback={ fallback={
<div class="flex items-center justify-center py-8"> <div class="flex items-center justify-center py-8">
@ -347,6 +352,7 @@ export const NodeDrawer: Component<NodeDrawerProps> = (props) => {
onCustomUrlChange={handleCustomUrlChange} onCustomUrlChange={handleCustomUrlChange}
/> />
</Suspense> </Suspense>
</Show>
</div> </div>
</div> </div>
); );