mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-04 21:26:22 +00:00
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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user