feat: auto-expand single node and filter-matched nodes

- When only one node exists, expand its list by default
- When filtering, auto-expand all nodes with matching results
- Use useMemo for filter/grouping performance

Backlog: mark Search/Filter as done

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-12 13:50:18 +01:00
parent e278d6a093
commit 6be309f183
2 changed files with 32 additions and 17 deletions

View File

@@ -1,6 +1,6 @@
'use client';
import { useState } from 'react';
import { useState, useMemo } from 'react';
import StatusBadge from './StatusBadge';
/**
@@ -14,18 +14,25 @@ export default function LXCList({ lxc, onPowerAction, onOpenConsole }) {
return <p className="text-text-secondary text-sm">No LXCs found.</p>;
}
const filtered = filter
? lxc.filter((item) =>
[item.name, item.node, item.status].some((v) => v.toLowerCase().includes(filter.toLowerCase())
))
: lxc;
const filtered = useMemo(() => {
if (!filter) return lxc;
const q = filter.toLowerCase();
return lxc.filter((item) =>
[item.name, item.node, item.status].some((v) => v.toLowerCase().includes(q)),
);
}, [lxc, filter]);
const groups = Object.groupBy(filtered, (item) => item.node);
const groups = useMemo(() => Object.groupBy(filtered, (item) => item.node), [filtered]);
const sortedNodes = Object.keys(groups).sort();
const autoExpand = sortedNodes.length === 1;
const total = filtered.length;
const running = filtered.filter((item) => item.status === 'running').length;
function toggleNode(node) {
setExpandedNodes((prev) => ({ ...prev, [node]: !prev[node] }));
}
return (
<div className="space-y-4">
{filter && (
@@ -47,12 +54,12 @@ export default function LXCList({ lxc, onPowerAction, onOpenConsole }) {
{sortedNodes.map((node) => {
const running = groups[node].filter((item) => item.status === 'running');
const stopped = groups[node].filter((item) => item.status !== 'running');
const expanded = expandedNodes[node] ?? false;
const expanded = autoExpand || expandedNodes[node] ?? false;
return (
<div key={node} className="rounded-xl border border-border-card bg-bg-card">
<button
onClick={() => setExpandedNodes((prev) => ({ ...prev, [node]: !prev[node] }))}
onClick={() => toggleNode(node)}
className="flex w-full items-center justify-between rounded-t-xl px-4 py-2.5 text-left"
>
<span className="text-sm font-medium text-text-primary">

View File

@@ -1,6 +1,6 @@
'use client';
import { useState } from 'react';
import { useState, useMemo } from 'react';
import StatusBadge from './StatusBadge';
/**
@@ -14,17 +14,25 @@ export default function VMList({ vms, onPowerAction, onOpenConsole }) {
return <p className="text-text-secondary text-sm">No VMs found.</p>;
}
const filtered = filter
? vms.filter((vm) =>
[vm.name, vm.node, vm.status].some((v) => v.toLowerCase().includes(filter.toLowerCase())))
: vms;
const filtered = useMemo(() => {
if (!filter) return vms;
const q = filter.toLowerCase();
return vms.filter((vm) =>
[vm.name, vm.node, vm.status].some((v) => v.toLowerCase().includes(q)),
);
}, [vms, filter]);
const groups = Object.groupBy(filtered, (vm) => vm.node);
const groups = useMemo(() => Object.groupBy(filtered, (vm) => vm.node), [filtered]);
const sortedNodes = Object.keys(groups).sort();
const autoExpand = sortedNodes.length === 1;
const total = filtered.length;
const running = filtered.filter((vm) => vm.status === 'running').length;
function toggleNode(node) {
setExpandedNodes((prev) => ({ ...prev, [node]: !prev[node] }));
}
return (
<div className="space-y-4">
{filter && (
@@ -46,12 +54,12 @@ export default function VMList({ vms, onPowerAction, onOpenConsole }) {
{sortedNodes.map((node) => {
const running = groups[node].filter((vm) => vm.status === 'running');
const stopped = groups[node].filter((vm) => vm.status !== 'running');
const expanded = expandedNodes[node] ?? false;
const expanded = autoExpand || expandedNodes[node] ?? false;
return (
<div key={node} className="rounded-xl border border-border-card bg-bg-card">
<button
onClick={() => setExpandedNodes((prev) => ({ ...prev, [node]: !prev[node] }))}
onClick={() => toggleNode(node)}
className="flex w-full items-center justify-between rounded-t-xl px-4 py-2.5 text-left"
>
<span className="text-sm font-medium text-text-primary">