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 [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,33 +562,34 @@ 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' }}
|
||||||
>
|
>
|
||||||
<Suspense
|
<Show when={discoveryActivated()}>
|
||||||
fallback={
|
<Suspense
|
||||||
<div class="flex items-center justify-center py-8">
|
fallback={
|
||||||
<div class="animate-spin h-6 w-6 border-2 border-blue-500 border-t-transparent rounded-full" />
|
<div class="flex items-center justify-center py-8">
|
||||||
<span class="ml-2 text-sm text-gray-500 dark:text-gray-400">
|
<div class="animate-spin h-6 w-6 border-2 border-blue-500 border-t-transparent rounded-full" />
|
||||||
Loading discovery...
|
<span class="ml-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
</span>
|
Loading discovery...
|
||||||
</div>
|
</span>
|
||||||
}
|
</div>
|
||||||
>
|
}
|
||||||
<DiscoveryTab
|
>
|
||||||
resourceType={discoveryResourceType()}
|
<DiscoveryTab
|
||||||
hostId={props.guest.node}
|
resourceType={discoveryResourceType()}
|
||||||
resourceId={String(props.guest.vmid)}
|
hostId={props.guest.node}
|
||||||
hostname={props.guest.name}
|
resourceId={String(props.guest.vmid)}
|
||||||
guestId={guestId()}
|
hostname={props.guest.name}
|
||||||
customUrl={props.customUrl}
|
guestId={guestId()}
|
||||||
onCustomUrlChange={(url) => props.onCustomUrlChange?.(guestId(), url)}
|
customUrl={props.customUrl}
|
||||||
/>
|
onCustomUrlChange={(url) => props.onCustomUrlChange?.(guestId(), url)}
|
||||||
</Suspense>
|
/>
|
||||||
|
</Suspense>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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,27 +533,29 @@ export const HostDrawer: Component<HostDrawerProps> = (props) => {
|
||||||
class={activeTab() === 'discovery' ? '' : 'hidden'}
|
class={activeTab() === 'discovery' ? '' : 'hidden'}
|
||||||
style={{ 'overflow-anchor': 'none' }}
|
style={{ 'overflow-anchor': 'none' }}
|
||||||
>
|
>
|
||||||
<Suspense
|
<Show when={discoveryActivated()}>
|
||||||
fallback={
|
<Suspense
|
||||||
<div class="flex items-center justify-center py-8">
|
fallback={
|
||||||
<div class="animate-spin h-6 w-6 border-2 border-blue-500 border-t-transparent rounded-full" />
|
<div class="flex items-center justify-center py-8">
|
||||||
<span class="ml-2 text-sm text-gray-500 dark:text-gray-400">
|
<div class="animate-spin h-6 w-6 border-2 border-blue-500 border-t-transparent rounded-full" />
|
||||||
Loading discovery...
|
<span class="ml-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
</span>
|
Loading discovery...
|
||||||
</div>
|
</span>
|
||||||
}
|
</div>
|
||||||
>
|
}
|
||||||
<DiscoveryTab
|
>
|
||||||
resourceType="host"
|
<DiscoveryTab
|
||||||
hostId={props.host.id}
|
resourceType="host"
|
||||||
resourceId={props.host.id} /* For hosts, typically same as hostId */
|
hostId={props.host.id}
|
||||||
hostname={props.host.hostname}
|
resourceId={props.host.id} /* For hosts, typically same as hostId */
|
||||||
guestId={props.host.id}
|
hostname={props.host.hostname}
|
||||||
customUrl={props.customUrl}
|
guestId={props.host.id}
|
||||||
onCustomUrlChange={(url) => props.onCustomUrlChange?.(props.host.id, url)}
|
customUrl={props.customUrl}
|
||||||
commandsEnabled={props.host.commandsEnabled}
|
onCustomUrlChange={(url) => props.onCustomUrlChange?.(props.host.id, url)}
|
||||||
/>
|
commandsEnabled={props.host.commandsEnabled}
|
||||||
</Suspense>
|
/>
|
||||||
|
</Suspense>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -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,26 +331,28 @@ export const NodeDrawer: Component<NodeDrawerProps> = (props) => {
|
||||||
class={activeTab() === 'discovery' ? '' : 'hidden'}
|
class={activeTab() === 'discovery' ? '' : 'hidden'}
|
||||||
style={{ 'overflow-anchor': 'none' }}
|
style={{ 'overflow-anchor': 'none' }}
|
||||||
>
|
>
|
||||||
<Suspense
|
<Show when={discoveryActivated()}>
|
||||||
fallback={
|
<Suspense
|
||||||
<div class="flex items-center justify-center py-8">
|
fallback={
|
||||||
<div class="animate-spin h-6 w-6 border-2 border-blue-500 border-t-transparent rounded-full" />
|
<div class="flex items-center justify-center py-8">
|
||||||
<span class="ml-2 text-sm text-gray-500 dark:text-gray-400">
|
<div class="animate-spin h-6 w-6 border-2 border-blue-500 border-t-transparent rounded-full" />
|
||||||
Loading discovery...
|
<span class="ml-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
</span>
|
Loading discovery...
|
||||||
</div>
|
</span>
|
||||||
}
|
</div>
|
||||||
>
|
}
|
||||||
<DiscoveryTab
|
>
|
||||||
resourceType="host" /* Assuming 'host' type works for PVE nodes discovery, or if backend treats them same */
|
<DiscoveryTab
|
||||||
hostId={props.node.id || props.node.name}
|
resourceType="host" /* Assuming 'host' type works for PVE nodes discovery, or if backend treats them same */
|
||||||
resourceId={props.node.id || props.node.name}
|
hostId={props.node.id || props.node.name}
|
||||||
hostname={props.node.name}
|
resourceId={props.node.id || props.node.name}
|
||||||
guestId={props.node.id || props.node.name}
|
hostname={props.node.name}
|
||||||
customUrl={fetchedCustomUrl()}
|
guestId={props.node.id || props.node.name}
|
||||||
onCustomUrlChange={handleCustomUrlChange}
|
customUrl={fetchedCustomUrl()}
|
||||||
/>
|
onCustomUrlChange={handleCustomUrlChange}
|
||||||
</Suspense>
|
/>
|
||||||
|
</Suspense>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue