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:
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState, useMemo } from 'react';
|
||||||
import StatusBadge from './StatusBadge';
|
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>;
|
return <p className="text-text-secondary text-sm">No LXCs found.</p>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const filtered = filter
|
const filtered = useMemo(() => {
|
||||||
? lxc.filter((item) =>
|
if (!filter) return lxc;
|
||||||
[item.name, item.node, item.status].some((v) => v.toLowerCase().includes(filter.toLowerCase())
|
const q = filter.toLowerCase();
|
||||||
))
|
return lxc.filter((item) =>
|
||||||
: lxc;
|
[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 sortedNodes = Object.keys(groups).sort();
|
||||||
|
const autoExpand = sortedNodes.length === 1;
|
||||||
|
|
||||||
const total = filtered.length;
|
const total = filtered.length;
|
||||||
const running = filtered.filter((item) => item.status === 'running').length;
|
const running = filtered.filter((item) => item.status === 'running').length;
|
||||||
|
|
||||||
|
function toggleNode(node) {
|
||||||
|
setExpandedNodes((prev) => ({ ...prev, [node]: !prev[node] }));
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{filter && (
|
{filter && (
|
||||||
@@ -47,12 +54,12 @@ export default function LXCList({ lxc, onPowerAction, onOpenConsole }) {
|
|||||||
{sortedNodes.map((node) => {
|
{sortedNodes.map((node) => {
|
||||||
const running = groups[node].filter((item) => item.status === 'running');
|
const running = groups[node].filter((item) => item.status === 'running');
|
||||||
const stopped = 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 (
|
return (
|
||||||
<div key={node} className="rounded-xl border border-border-card bg-bg-card">
|
<div key={node} className="rounded-xl border border-border-card bg-bg-card">
|
||||||
<button
|
<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"
|
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">
|
<span className="text-sm font-medium text-text-primary">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState, useMemo } from 'react';
|
||||||
import StatusBadge from './StatusBadge';
|
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>;
|
return <p className="text-text-secondary text-sm">No VMs found.</p>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const filtered = filter
|
const filtered = useMemo(() => {
|
||||||
? vms.filter((vm) =>
|
if (!filter) return vms;
|
||||||
[vm.name, vm.node, vm.status].some((v) => v.toLowerCase().includes(filter.toLowerCase())))
|
const q = filter.toLowerCase();
|
||||||
: vms;
|
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 sortedNodes = Object.keys(groups).sort();
|
||||||
|
const autoExpand = sortedNodes.length === 1;
|
||||||
|
|
||||||
const total = filtered.length;
|
const total = filtered.length;
|
||||||
const running = filtered.filter((vm) => vm.status === 'running').length;
|
const running = filtered.filter((vm) => vm.status === 'running').length;
|
||||||
|
|
||||||
|
function toggleNode(node) {
|
||||||
|
setExpandedNodes((prev) => ({ ...prev, [node]: !prev[node] }));
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{filter && (
|
{filter && (
|
||||||
@@ -46,12 +54,12 @@ export default function VMList({ vms, onPowerAction, onOpenConsole }) {
|
|||||||
{sortedNodes.map((node) => {
|
{sortedNodes.map((node) => {
|
||||||
const running = groups[node].filter((vm) => vm.status === 'running');
|
const running = groups[node].filter((vm) => vm.status === 'running');
|
||||||
const stopped = 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 (
|
return (
|
||||||
<div key={node} className="rounded-xl border border-border-card bg-bg-card">
|
<div key={node} className="rounded-xl border border-border-card bg-bg-card">
|
||||||
<button
|
<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"
|
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">
|
<span className="text-sm font-medium text-text-primary">
|
||||||
|
|||||||
Reference in New Issue
Block a user