sort by Disk I/O and Net I/O - the header cycles read/write and down/up

This commit is contained in:
Jarek Krochmalski committed 2026-08-27 07:34:38 +02:00
1 parent b1e7518096
commit 0d516be4dd
8 files changed
+217 -18

No files matched your search

+45 -5
View File
@@ -5,6 +5,7 @@
import { columnResize } from '$lib/actions/column-resize';
import { gridPreferencesStore } from '$lib/stores/grid-preferences';
import { getAllColumnConfigs } from '$lib/config/grid-columns';
import { nextIoSortState } from '$lib/utils/io-sort-cycle';
import ColumnSettingsPopover from '$lib/components/ColumnSettingsPopover.svelte';
import { Skeleton } from '$lib/components/ui/skeleton';
import type { GridId, ColumnConfig, ColumnPreference } from '$lib/types';
@@ -132,8 +133,8 @@
const orderedColumns = $derived.by(() => {
const prefs = gridPrefs[gridId];
if (!prefs?.columns?.length) {
// Default: all configurable columns visible
return columnConfigs.filter((c) => !c.fixed).map((c) => c.id);
// Default: configurable columns visible (honoring per-column defaultVisible)
return columnConfigs.filter((c) => !c.fixed && c.defaultVisible !== false).map((c) => c.id);
}
// Filter out fixed columns - they're rendered separately via fixedStartCols/fixedEndCols
const fixedIds = new Set([...fixedStartCols, ...fixedEndCols]);
@@ -369,9 +370,36 @@
});
// Sort helpers
function toggleSort(field: string) {
// One-char badge for the active I/O sub-metric next to a sortCycle column's label.
// Char + colour mirror the cell glyphs (Disk r=green/w=yellow, Net d=blue/u=orange).
const IO_METRIC_LABELS: Record<string, string> = {
diskRead: 'r', diskWrite: 'w', netRx: 'd', netTx: 'u'
};
const IO_METRIC_COLORS: Record<string, string> = {
diskRead: 'text-green-400', diskWrite: 'text-yellow-400',
netRx: 'text-blue-400', netTx: 'text-orange-400'
};
function ioMetricLabel(field: string): string {
return IO_METRIC_LABELS[field] ?? '';
}
function ioMetricColor(field: string): string {
return IO_METRIC_COLORS[field] ?? 'text-primary';
}
function toggleSort(colId: string) {
if (!onSortChange) return;
const cycle = columnConfigMap.get(colId)?.sortCycle;
if (cycle?.length) {
// A two-metric column (e.g. Disk I/O) cycles read/write x asc/desc (#1111).
onSortChange(nextIoSortState(cycle as any, {
field: sortState?.field ?? '',
direction: sortState?.direction ?? 'asc'
}));
return;
}
const field = getSortField(colId);
const newState: DataGridSortState = sortState?.field === field
? { field, direction: sortState.direction === 'asc' ? 'desc' : 'asc' }
: { field, direction: 'asc' };
@@ -769,13 +797,25 @@
{#if headerCell}
{@render headerCell(colConfig, sortState)}
{:else if isSortable(colId)}
{@const cycleActive = colConfig.sortCycle?.some((s) => s.field === sortState?.field)}
<button
type="button"
onclick={() => toggleSort(getSortField(colId))}
onclick={() => toggleSort(colId)}
class="flex items-center gap-1 hover:text-foreground transition-colors w-full {colConfig.align === 'right' ? 'justify-end' : colConfig.align === 'center' ? 'justify-center' : ''}"
>
{colConfig.label}
{#if sortState?.field === getSortField(colId)}
{#if colConfig.sortCycle}
{#if cycleActive}
<span class="text-xs font-semibold {ioMetricColor(sortState!.field)}">{ioMetricLabel(sortState!.field)}</span>
{#if sortState!.direction === 'asc'}
<ArrowUp class="w-3 h-3" />
{:else}
<ArrowDown class="w-3 h-3" />
{/if}
{:else}
<ArrowUpDown class="w-3 h-3 opacity-30" />
{/if}
{:else if sortState?.field === getSortField(colId)}
{#if sortState.direction === 'asc'}
<ArrowUp class="w-3 h-3" />
{:else}
+1 -1
View File
@@ -101,7 +101,7 @@ export interface DataGridContext<T = unknown> {
// Sort helpers
sortState: DataGridSortState | undefined;
toggleSort: (field: string) => void;
toggleSort: (colId: string) => void;
// Resize helpers
handleResize: (colId: string, width: number) => void;
+22 -5
View File
@@ -1,5 +1,21 @@
import type { ColumnConfig, GridId } from '$lib/types';
// The Disk I/O and Net I/O columns each hold two metrics; a header click cycles
// through read/write (or rx/tx) x desc/asc so both hogs are reachable (#1111).
// "biggest first" (desc) leads because hunting a hog is the common case.
const DISK_IO_SORT_CYCLE = [
{ field: 'diskRead', direction: 'desc' as const },
{ field: 'diskRead', direction: 'asc' as const },
{ field: 'diskWrite', direction: 'desc' as const },
{ field: 'diskWrite', direction: 'asc' as const }
];
const NET_IO_SORT_CYCLE = [
{ field: 'netRx', direction: 'desc' as const },
{ field: 'netRx', direction: 'asc' as const },
{ field: 'netTx', direction: 'desc' as const },
{ field: 'netTx', direction: 'asc' as const }
];
// Container grid columns
export const containerColumns: ColumnConfig[] = [
{ id: 'select', label: '', fixed: 'start', width: 32, resizable: false },
@@ -11,8 +27,8 @@ export const containerColumns: ColumnConfig[] = [
{ id: 'restartCount', label: 'Restarts', width: 70, minWidth: 50 },
{ id: 'cpu', label: 'CPU', sortable: true, sortField: 'cpu', width: 50, minWidth: 40, align: 'right' },
{ id: 'memory', label: 'Memory', sortable: true, sortField: 'memory', width: 95, minWidth: 70, align: 'right' },
{ id: 'networkIO', label: 'Net I/O', width: 85, minWidth: 70, align: 'right' },
{ id: 'diskIO', label: 'Disk I/O', width: 85, minWidth: 70, align: 'right' },
{ id: 'networkIO', label: 'Net I/O', width: 85, minWidth: 70, align: 'right', sortable: true, sortCycle: NET_IO_SORT_CYCLE },
{ id: 'diskIO', label: 'Disk I/O', width: 85, minWidth: 70, align: 'right', sortable: true, sortCycle: DISK_IO_SORT_CYCLE },
{ id: 'ip', label: 'IP', sortable: true, sortField: 'ip', width: 100, minWidth: 80 },
{ id: 'ports', label: 'Ports', sortable: true, sortField: 'ports', width: 120, minWidth: 60 },
{ id: 'autoUpdate', label: 'Auto-update', width: 95, minWidth: 70 },
@@ -60,12 +76,13 @@ export const stackColumns: ColumnConfig[] = [
{ id: 'name', label: 'Name', sortable: true, sortField: 'name', width: 180, minWidth: 100, grow: true },
{ id: 'status', label: 'Status', sortable: true, sortField: 'status', width: 120, minWidth: 90 },
{ id: 'source', label: 'Source', width: 100, minWidth: 100, noTruncate: true },
{ id: 'webhook', label: 'Webhook', width: 90, minWidth: 70, defaultVisible: false, hint: 'Git-stack webhook id and URL (for wiring Gitea/GitHub/GitLab)' },
{ id: 'location', label: 'Location', width: 180, minWidth: 100 },
{ id: 'containers', label: 'Containers', sortable: true, sortField: 'containers', width: 100, minWidth: 70 },
{ id: 'cpu', label: 'CPU', sortable: true, sortField: 'cpu', width: 60, minWidth: 50, align: 'right' },
{ id: 'memory', label: 'Memory', sortable: true, sortField: 'memory', width: 70, minWidth: 50, align: 'right' },
{ id: 'networkIO', label: 'Net I/O', width: 100, minWidth: 70, align: 'right' },
{ id: 'diskIO', label: 'Disk I/O', width: 100, minWidth: 70, align: 'right' },
{ id: 'networkIO', label: 'Net I/O', width: 100, minWidth: 70, align: 'right', sortable: true, sortCycle: NET_IO_SORT_CYCLE },
{ id: 'diskIO', label: 'Disk I/O', width: 100, minWidth: 70, align: 'right', sortable: true, sortCycle: DISK_IO_SORT_CYCLE },
{ id: 'networks', label: 'Networks', width: 80, minWidth: 60 },
{ id: 'volumes', label: 'Volumes', width: 80, minWidth: 60 },
{ id: 'actions', label: '', fixed: 'end', width: 180, resizable: false }
@@ -227,7 +244,7 @@ export function getFixedEndColumns(gridId: GridId): ColumnConfig[] {
export function getDefaultColumnPreferences(gridId: GridId): { id: string; visible: boolean }[] {
return getConfigurableColumns(gridId).map((col) => ({
id: col.id,
visible: true
visible: col.defaultVisible !== false
}));
}
+4
View File
@@ -201,6 +201,10 @@ export interface ColumnConfig {
grow?: boolean; // If true, column expands to fill remaining space
noTruncate?: boolean; // If true, content won't be truncated with ellipsis
hint?: string; // Tooltip on column header
defaultVisible?: boolean; // If false, column is hidden by default (user can enable it in preferences)
// A column holding two metrics (e.g. Disk I/O = read/write) cycles a header click
// through these (sortField, direction) states instead of a plain asc/desc toggle (#1111).
sortCycle?: { field: string; direction: 'asc' | 'desc' }[];
}
export interface ColumnPreference {
+22
View File
@@ -0,0 +1,22 @@
/**
* The two I/O grid columns (Disk I/O, Net I/O) each hold two metrics, so a header
* click cycles through all four (metric, direction) states rather than the usual
* asc/desc toggle (#1111). Pure so it can be unit-tested and shared by the containers
* and stacks lists.
*/
export type IoSortState<F extends string> = { field: F; direction: 'asc' | 'desc' };
/**
* Given a column's ordered cycle and the current sort, return the next state.
* If the current sort is not on this column, start at the first cycle entry
* (read desc / rx desc - "biggest first" is the useful default for hunting hogs).
*/
export function nextIoSortState<F extends string>(
cycle: IoSortState<F>[],
current: { field: F; direction: 'asc' | 'desc' }
): IoSortState<F> {
const idx = cycle.findIndex((s) => s.field === current.field && s.direction === current.direction);
if (idx === -1) return cycle[0];
return cycle[(idx + 1) % cycle.length];
}