feat: sortable resource tables and richer dashboard stack-health columns (#1498)

Make the Resources Images, Volumes, and Networks tables sortable with a
shared useTableSort hook and SortableTableHead, move the Images scan-history
control into the Images tab header, and keep the network List/Topology toggle
anchored with Create Network visible in both modes.

Rework the dashboard Stack health table: drop the redundant Host column, add
sortable Stack/Up/CPU/Mem headers, and add Source (local/git) and Port columns.
The status endpoint now labels each stack with its git/local source, computed
outside the cache so linking changes show immediately.

Extract a reusable CreateNetworkDialog and add a create-network action to the
stack-detail Networking tab.
This commit is contained in:
Anso
2026-06-28 05:06:04 -04:00
committed by GitHub
parent cf0db36e78
commit 60536aa614
11 changed files with 512 additions and 190 deletions
@@ -0,0 +1,46 @@
import { describe, it, expect } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useTableSort } from '../useTableSort';
interface Row { name: string; size: number }
const ITEMS: Row[] = [
{ name: 'beta', size: 30 },
{ name: 'alpha', size: 10 },
{ name: 'gamma', size: 20 },
];
const COMPARATORS = {
name: (a: Row, b: Row) => a.name.localeCompare(b.name),
size: (a: Row, b: Row) => a.size - b.size,
};
describe('useTableSort', () => {
it('sorts by the initial key and direction', () => {
const { result } = renderHook(() => useTableSort(ITEMS, COMPARATORS, 'name'));
expect(result.current.sorted.map(r => r.name)).toEqual(['alpha', 'beta', 'gamma']);
expect(result.current.sortKey).toBe('name');
expect(result.current.sortDir).toBe('asc');
});
it('flips direction when toggling the active key', () => {
const { result } = renderHook(() => useTableSort(ITEMS, COMPARATORS, 'name'));
act(() => result.current.toggleSort('name'));
expect(result.current.sortDir).toBe('desc');
expect(result.current.sorted.map(r => r.name)).toEqual(['gamma', 'beta', 'alpha']);
});
it('switches key and resets direction to asc', () => {
const { result } = renderHook(() => useTableSort(ITEMS, COMPARATORS, 'name', 'desc'));
act(() => result.current.toggleSort('size'));
expect(result.current.sortKey).toBe('size');
expect(result.current.sortDir).toBe('asc');
expect(result.current.sorted.map(r => r.size)).toEqual([10, 20, 30]);
});
it('does not mutate the input array', () => {
const input = [...ITEMS];
renderHook(() => useTableSort(input, COMPARATORS, 'size'));
expect(input.map(r => r.name)).toEqual(['beta', 'alpha', 'gamma']);
});
});
+33
View File
@@ -0,0 +1,33 @@
import { useMemo, useState } from 'react';
export type SortDir = 'asc' | 'desc';
/**
* Shared client-side table sort. Pass a stable `comparators` map (define it at
* module scope so the memo does not re-sort every render). This is the standard
* sort behavior for data tables, mirroring the Security Images tab.
*/
export function useTableSort<T, K extends string>(
items: T[],
comparators: Record<K, (a: T, b: T) => number>,
// NoInfer so K is inferred from `comparators` only; otherwise `initialKey`
// collapses K to a single literal and the column keys fail to type-check.
initialKey: NoInfer<K>,
initialDir: SortDir = 'asc',
) {
const [sortKey, setSortKey] = useState<K>(initialKey);
const [sortDir, setSortDir] = useState<SortDir>(initialDir);
const sorted = useMemo(() => {
const cmp = comparators[sortKey];
const dir = sortDir === 'asc' ? 1 : -1;
return [...items].sort((a, b) => cmp(a, b) * dir);
}, [items, comparators, sortKey, sortDir]);
const toggleSort = (key: K) => {
if (key === sortKey) setSortDir((d) => (d === 'asc' ? 'desc' : 'asc'));
else { setSortKey(key); setSortDir('asc'); }
};
return { sorted, sortKey, sortDir, toggleSort };
}