mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-04-28 03:20:11 +00:00
fix(frontend): defer discovery tab initialization until opened
This commit is contained in:
parent
d05a00b931
commit
fde4d9124e
5 changed files with 689 additions and 245 deletions
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -107,12 +107,16 @@ export const GuestDrawer: Component<GuestDrawerProps> = (props) => {
|
|||
};
|
||||
|
||||
const [activeTab, setActiveTab] = createSignal<'overview' | 'discovery'>('overview');
|
||||
const [discoveryActivated, setDiscoveryActivated] = createSignal(false);
|
||||
|
||||
// All tabs are always rendered (hidden via CSS) to avoid any DOM
|
||||
// mount/unmount during tab switches. Mounting new components inside
|
||||
// a <For>-rendered table row causes SolidJS to recreate the row,
|
||||
// which detaches the element and resets the scroll container.
|
||||
const switchTab = (tab: 'overview' | 'discovery') => {
|
||||
if (tab === 'discovery') {
|
||||
setDiscoveryActivated(true);
|
||||
}
|
||||
setActiveTab(tab);
|
||||
};
|
||||
|
||||
|
|
@ -558,33 +562,34 @@ export const GuestDrawer: Component<GuestDrawerProps> = (props) => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Always rendered, hidden via CSS. Wrapped in a local Suspense
|
||||
so DiscoveryTab's createResource loading state doesn't bubble
|
||||
up to the app-level Suspense and replace the entire page. */}
|
||||
{/* Defer discovery initialization until the tab is first opened, then keep it
|
||||
mounted so subsequent tab switches don't churn the row. */}
|
||||
<div
|
||||
class={activeTab() === 'discovery' ? '' : 'hidden'}
|
||||
style={{ 'overflow-anchor': 'none' }}
|
||||
>
|
||||
<Suspense
|
||||
fallback={
|
||||
<div class="flex items-center justify-center py-8">
|
||||
<div class="animate-spin h-6 w-6 border-2 border-blue-500 border-t-transparent rounded-full" />
|
||||
<span class="ml-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Loading discovery...
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<DiscoveryTab
|
||||
resourceType={discoveryResourceType()}
|
||||
hostId={props.guest.node}
|
||||
resourceId={String(props.guest.vmid)}
|
||||
hostname={props.guest.name}
|
||||
guestId={guestId()}
|
||||
customUrl={props.customUrl}
|
||||
onCustomUrlChange={(url) => props.onCustomUrlChange?.(guestId(), url)}
|
||||
/>
|
||||
</Suspense>
|
||||
<Show when={discoveryActivated()}>
|
||||
<Suspense
|
||||
fallback={
|
||||
<div class="flex items-center justify-center py-8">
|
||||
<div class="animate-spin h-6 w-6 border-2 border-blue-500 border-t-transparent rounded-full" />
|
||||
<span class="ml-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Loading discovery...
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<DiscoveryTab
|
||||
resourceType={discoveryResourceType()}
|
||||
hostId={props.guest.node}
|
||||
resourceId={String(props.guest.vmid)}
|
||||
hostname={props.guest.name}
|
||||
guestId={guestId()}
|
||||
customUrl={props.customUrl}
|
||||
onCustomUrlChange={(url) => props.onCustomUrlChange?.(guestId(), url)}
|
||||
/>
|
||||
</Suspense>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ interface HostDrawerProps {
|
|||
|
||||
export const HostDrawer: Component<HostDrawerProps> = (props) => {
|
||||
const [activeTab, setActiveTab] = createSignal<'overview' | 'discovery'>('overview');
|
||||
const [discoveryActivated, setDiscoveryActivated] = createSignal(false);
|
||||
const [historyRange, setHistoryRange] = useDrawerHistoryRange(`host:${props.host.id}`);
|
||||
const [editingUrl, setEditingUrl] = createSignal(false);
|
||||
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 switchTab = (tab: 'overview' | 'discovery') => {
|
||||
if (tab === 'discovery') {
|
||||
setDiscoveryActivated(true);
|
||||
}
|
||||
setActiveTab(tab);
|
||||
};
|
||||
|
||||
|
|
@ -529,27 +533,29 @@ export const HostDrawer: Component<HostDrawerProps> = (props) => {
|
|||
class={activeTab() === 'discovery' ? '' : 'hidden'}
|
||||
style={{ 'overflow-anchor': 'none' }}
|
||||
>
|
||||
<Suspense
|
||||
fallback={
|
||||
<div class="flex items-center justify-center py-8">
|
||||
<div class="animate-spin h-6 w-6 border-2 border-blue-500 border-t-transparent rounded-full" />
|
||||
<span class="ml-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Loading discovery...
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<DiscoveryTab
|
||||
resourceType="host"
|
||||
hostId={props.host.id}
|
||||
resourceId={props.host.id} /* For hosts, typically same as hostId */
|
||||
hostname={props.host.hostname}
|
||||
guestId={props.host.id}
|
||||
customUrl={props.customUrl}
|
||||
onCustomUrlChange={(url) => props.onCustomUrlChange?.(props.host.id, url)}
|
||||
commandsEnabled={props.host.commandsEnabled}
|
||||
/>
|
||||
</Suspense>
|
||||
<Show when={discoveryActivated()}>
|
||||
<Suspense
|
||||
fallback={
|
||||
<div class="flex items-center justify-center py-8">
|
||||
<div class="animate-spin h-6 w-6 border-2 border-blue-500 border-t-transparent rounded-full" />
|
||||
<span class="ml-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Loading discovery...
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<DiscoveryTab
|
||||
resourceType="host"
|
||||
hostId={props.host.id}
|
||||
resourceId={props.host.id} /* For hosts, typically same as hostId */
|
||||
hostname={props.host.hostname}
|
||||
guestId={props.host.id}
|
||||
customUrl={props.customUrl}
|
||||
onCustomUrlChange={(url) => props.onCustomUrlChange?.(props.host.id, url)}
|
||||
commandsEnabled={props.host.commandsEnabled}
|
||||
/>
|
||||
</Suspense>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -17,6 +17,7 @@ interface NodeDrawerProps {
|
|||
|
||||
export const NodeDrawer: Component<NodeDrawerProps> = (props) => {
|
||||
const [activeTab, setActiveTab] = createSignal<'overview' | 'discovery'>('overview');
|
||||
const [discoveryActivated, setDiscoveryActivated] = createSignal(false);
|
||||
const [historyRange, setHistoryRange] = useDrawerHistoryRange(
|
||||
`node:${props.node.id || props.node.name}`,
|
||||
);
|
||||
|
|
@ -49,6 +50,9 @@ export const NodeDrawer: Component<NodeDrawerProps> = (props) => {
|
|||
};
|
||||
|
||||
const switchTab = (tab: 'overview' | 'discovery') => {
|
||||
if (tab === 'discovery') {
|
||||
setDiscoveryActivated(true);
|
||||
}
|
||||
setActiveTab(tab);
|
||||
};
|
||||
|
||||
|
|
@ -327,26 +331,28 @@ export const NodeDrawer: Component<NodeDrawerProps> = (props) => {
|
|||
class={activeTab() === 'discovery' ? '' : 'hidden'}
|
||||
style={{ 'overflow-anchor': 'none' }}
|
||||
>
|
||||
<Suspense
|
||||
fallback={
|
||||
<div class="flex items-center justify-center py-8">
|
||||
<div class="animate-spin h-6 w-6 border-2 border-blue-500 border-t-transparent rounded-full" />
|
||||
<span class="ml-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Loading discovery...
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<DiscoveryTab
|
||||
resourceType="host" /* Assuming 'host' type works for PVE nodes discovery, or if backend treats them same */
|
||||
hostId={props.node.id || props.node.name}
|
||||
resourceId={props.node.id || props.node.name}
|
||||
hostname={props.node.name}
|
||||
guestId={props.node.id || props.node.name}
|
||||
customUrl={fetchedCustomUrl()}
|
||||
onCustomUrlChange={handleCustomUrlChange}
|
||||
/>
|
||||
</Suspense>
|
||||
<Show when={discoveryActivated()}>
|
||||
<Suspense
|
||||
fallback={
|
||||
<div class="flex items-center justify-center py-8">
|
||||
<div class="animate-spin h-6 w-6 border-2 border-blue-500 border-t-transparent rounded-full" />
|
||||
<span class="ml-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Loading discovery...
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<DiscoveryTab
|
||||
resourceType="host" /* Assuming 'host' type works for PVE nodes discovery, or if backend treats them same */
|
||||
hostId={props.node.id || props.node.name}
|
||||
resourceId={props.node.id || props.node.name}
|
||||
hostname={props.node.name}
|
||||
guestId={props.node.id || props.node.name}
|
||||
customUrl={fetchedCustomUrl()}
|
||||
onCustomUrlChange={handleCustomUrlChange}
|
||||
/>
|
||||
</Suspense>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue