diff --git a/frontend-modern/src/components/Dashboard/Dashboard.tsx b/frontend-modern/src/components/Dashboard/Dashboard.tsx index 8b2e822..0e256ee 100644 --- a/frontend-modern/src/components/Dashboard/Dashboard.tsx +++ b/frontend-modern/src/components/Dashboard/Dashboard.tsx @@ -1,4 +1,5 @@ import { createSignal, createMemo, createEffect, For, Show, onMount } from 'solid-js'; +import { createStore, reconcile } from 'solid-js/store'; import { useNavigate } from '@solidjs/router'; import type { VM, Container, Node } from '@/types/api'; import { GuestRow } from './GuestRow'; @@ -41,6 +42,16 @@ export function Dashboard(props: DashboardProps) { const [selectedNode, setSelectedNode] = createSignal(null); const [guestMetadata, setGuestMetadata] = createSignal>({}); + // Stable guest store using reconcile to prevent row remounting during websocket updates + const guestKey = (g: VM | Container) => + g.id ?? (g.instance === g.node ? `${g.node}-${g.vmid}` : `${g.instance}-${g.node}-${g.vmid}`); + const [guestStore, setGuestStore] = createStore<(VM | Container)[]>([]); + + // Reconcile guests whenever props change + createEffect(() => { + setGuestStore(reconcile([...props.vms, ...props.containers], { key: guestKey, merge: true })); + }); + // Initialize from localStorage with proper type checking const [viewMode, setViewMode] = usePersistentSignal('dashboardViewMode', 'all', { deserialize: (raw) => (raw === 'all' || raw === 'vm' || raw === 'lxc' ? raw : 'all'), @@ -237,12 +248,8 @@ export function Dashboard(props: DashboardProps) { return () => document.removeEventListener('keydown', handleKeyDown); }); - // Combine VMs and containers into a single list - const allGuests = createMemo(() => { - const vms = props.vms || []; - const containers = props.containers || []; - return [...vms, ...containers]; - }); + // Use the stable guest store + const allGuests = createMemo(() => guestStore); // Filter guests based on current settings const filteredGuests = createMemo(() => { diff --git a/frontend-modern/src/components/Dashboard/GuestRow.tsx b/frontend-modern/src/components/Dashboard/GuestRow.tsx index 7fc44b3..fd19a02 100644 --- a/frontend-modern/src/components/Dashboard/GuestRow.tsx +++ b/frontend-modern/src/components/Dashboard/GuestRow.tsx @@ -192,8 +192,11 @@ export function GuestRow(props: GuestRowProps) { if (isEditingUrl()) { const handleGlobalClick = (e: MouseEvent) => { const target = e.target as HTMLElement; - // If clicking outside the editor, close it and prevent the click - if (!target.closest('[data-url-editor]') && currentlyEditingGuestId() === guestId()) { + // Allow clicking another guest name to switch editing + const isClickingGuestName = target.closest('[data-guest-name-editable]'); + + // If clicking outside the editor (and not another guest name), close it and prevent the click + if (!target.closest('[data-url-editor]') && !isClickingGuestName && currentlyEditingGuestId() === guestId()) { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); @@ -203,7 +206,9 @@ export function GuestRow(props: GuestRowProps) { const handleGlobalMouseDown = (e: MouseEvent) => { const target = e.target as HTMLElement; - if (!target.closest('[data-url-editor]') && currentlyEditingGuestId() === guestId()) { + const isClickingGuestName = target.closest('[data-guest-name-editable]'); + + if (!target.closest('[data-url-editor]') && !isClickingGuestName && currentlyEditingGuestId() === guestId()) { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); @@ -407,6 +412,7 @@ export function GuestRow(props: GuestRowProps) { style="cursor: text;" title={`${props.guest.name}${customUrl() ? ' - Click to edit URL' : ' - Click to add URL'}`} onClick={startEditingUrl} + data-guest-name-editable > {props.guest.name}