improve: group guests by node in Guest URLs table

- Matches main dashboard layout with node header rows
- Guests sorted by VMID within each node
- Removed redundant Node column since it's shown in headers
- Better visual organization and consistency
This commit is contained in:
Pulse Monitor
2025-08-20 19:27:18 +00:00
parent e2a82af71e
commit 2fce180343
@@ -22,22 +22,38 @@ export function GuestURLs(props: GuestURLsProps) {
const allGuests = createMemo(() => {
const vms = (state.vms || []) as VM[];
const containers = (state.containers || []) as Container[];
const combined = [...vms, ...containers];
// Sort by name
return combined.sort((a, b) => a.name.localeCompare(b.name));
return [...vms, ...containers];
});
// Filter guests based on search
const filteredGuests = createMemo(() => {
// Filter and group guests by node
const groupedGuests = createMemo(() => {
const search = searchTerm().toLowerCase();
if (!search) return allGuests();
let guests = allGuests();
return allGuests().filter(guest =>
guest.name.toLowerCase().includes(search) ||
guest.vmid.toString().includes(search) ||
guest.node.toLowerCase().includes(search)
);
// Apply search filter
if (search) {
guests = guests.filter(guest =>
guest.name.toLowerCase().includes(search) ||
guest.vmid.toString().includes(search) ||
guest.node.toLowerCase().includes(search)
);
}
// Group by node
const groups: Record<string, (VM | Container)[]> = {};
guests.forEach(guest => {
if (!groups[guest.node]) {
groups[guest.node] = [];
}
groups[guest.node].push(guest);
});
// Sort guests within each node by VMID
Object.keys(groups).forEach(node => {
groups[node] = groups[node].sort((a, b) => a.vmid - b.vmid);
});
return groups;
});
// Load saved URLs on mount
@@ -139,9 +155,6 @@ export function GuestURLs(props: GuestURLsProps) {
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
VMID
</th>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Node
</th>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Custom URL
</th>
@@ -151,85 +164,97 @@ export function GuestURLs(props: GuestURLsProps) {
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
<For each={filteredGuests()} fallback={
<Show when={Object.keys(groupedGuests()).length === 0} fallback={
<For each={Object.entries(groupedGuests()).sort(([a], [b]) => a.localeCompare(b))}>
{([node, guests]) => (
<>
{/* Node header row */}
<tr class="node-header bg-gray-50 dark:bg-gray-700/50 font-semibold text-gray-700 dark:text-gray-300 text-xs">
<td colspan="5" class="px-3 py-1 text-xs font-medium text-gray-500 dark:text-gray-400">
{node}
</td>
</tr>
{/* Guest rows for this node */}
<For each={guests}>
{(guest) => {
const guestId = guest.id || `${guest.instance}-${guest.name}-${guest.vmid}`;
const config = guestURLs()[guestId];
const url = getURL(guestId);
return (
<tr class="hover:bg-gray-50 dark:hover:bg-gray-900/50 transition-colors">
<td class="px-3 py-1.5">
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
{guest.name}
</div>
</td>
<td class="px-3 py-1.5">
<span class={`inline-block px-1.5 py-0.5 text-xs font-medium rounded ${
guest.type === 'qemu'
? 'bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300'
: 'bg-green-100 text-green-700 dark:bg-green-900/50 dark:text-green-300'
}`}>
{guest.type === 'qemu' ? 'VM' : 'LXC'}
</span>
</td>
<td class="px-3 py-1.5 text-sm text-gray-600 dark:text-gray-400">
{guest.vmid}
</td>
<td class="px-3 py-1.5">
<input
type="text"
placeholder="https://192.168.1.100:8006"
value={config?.url || ''}
onInput={(e) => updateGuestURL(guestId, e.currentTarget.value)}
class="w-full px-2 py-0.5 text-sm border border-gray-300 dark:border-gray-600 rounded
bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100
focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</td>
<td class="px-3 py-1.5">
<div class="flex items-center gap-2">
<Show when={url}>
<a
href={url}
target="_blank"
rel="noopener noreferrer"
class="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
title="Test URL"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
</a>
</Show>
<Show when={config?.url}>
<button
onClick={() => clearGuestURL(guestId)}
class="text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"
title="Clear URL"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</Show>
</div>
</td>
</tr>
);
}}
</For>
</>
)}
</For>
}>
<tr>
<td colspan="6" class="px-4 py-8 text-center text-sm text-gray-500 dark:text-gray-400">
<td colspan="5" class="px-4 py-8 text-center text-sm text-gray-500 dark:text-gray-400">
No guests found
</td>
</tr>
}>
{(guest) => {
const guestId = guest.id || `${guest.instance}-${guest.name}-${guest.vmid}`;
const config = guestURLs()[guestId];
const url = getURL(guestId);
return (
<tr class="hover:bg-gray-50 dark:hover:bg-gray-900/50 transition-colors">
<td class="px-3 py-1.5">
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
{guest.name}
</div>
</td>
<td class="px-3 py-1.5">
<span class={`inline-block px-1.5 py-0.5 text-xs font-medium rounded ${
guest.type === 'qemu'
? 'bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300'
: 'bg-green-100 text-green-700 dark:bg-green-900/50 dark:text-green-300'
}`}>
{guest.type === 'qemu' ? 'VM' : 'LXC'}
</span>
</td>
<td class="px-3 py-1.5 text-sm text-gray-600 dark:text-gray-400">
{guest.vmid}
</td>
<td class="px-3 py-1.5 text-sm text-gray-600 dark:text-gray-400">
{guest.node}
</td>
<td class="px-3 py-1.5">
<input
type="text"
placeholder="https://192.168.1.100:8006"
value={config?.url || ''}
onInput={(e) => updateGuestURL(guestId, e.currentTarget.value)}
class="w-full px-2 py-0.5 text-sm border border-gray-300 dark:border-gray-600 rounded
bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100
focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</td>
<td class="px-3 py-1.5">
<div class="flex items-center gap-2">
<Show when={url}>
<a
href={url}
target="_blank"
rel="noopener noreferrer"
class="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
title="Test URL"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
</a>
</Show>
<Show when={config?.url}>
<button
onClick={() => clearGuestURL(guestId)}
class="text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"
title="Clear URL"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</Show>
</div>
</td>
</tr>
);
}}
</For>
</Show>
</tbody>
</table>
</div>