sort by Disk I/O and Net I/O - the header cycles read/write and down/up
This commit is contained in:
8 files changed
+217
-18
No files matched your search
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
@@ -106,9 +106,10 @@
|
||||
// Track change detection for stat highlighting (UI-only, stays in component)
|
||||
let changedFields = $state<Map<string, Set<string>>>(new Map());
|
||||
|
||||
type SortField = 'name' | 'image' | 'state' | 'health' | 'uptime' | 'stack' | 'ip' | 'cpu' | 'memory' | 'ports';
|
||||
type SortField = 'name' | 'image' | 'state' | 'health' | 'uptime' | 'stack' | 'ip' | 'cpu' | 'memory' | 'ports' | 'diskRead' | 'diskWrite' | 'netRx' | 'netTx';
|
||||
type SortDirection = 'asc' | 'desc';
|
||||
|
||||
|
||||
// Data from persistent store (survives page navigation)
|
||||
const containers = $derived($containerStore.data);
|
||||
// User-set per-container icon overrides for the current env (name -> icon), batch-loaded.
|
||||
@@ -862,6 +863,18 @@
|
||||
const memB = containerStats.get(b.id)?.memoryUsage ?? -1;
|
||||
cmp = memA - memB;
|
||||
break;
|
||||
case 'diskRead':
|
||||
cmp = (containerStats.get(a.id)?.blockRead ?? -1) - (containerStats.get(b.id)?.blockRead ?? -1);
|
||||
break;
|
||||
case 'diskWrite':
|
||||
cmp = (containerStats.get(a.id)?.blockWrite ?? -1) - (containerStats.get(b.id)?.blockWrite ?? -1);
|
||||
break;
|
||||
case 'netRx':
|
||||
cmp = (containerStats.get(a.id)?.networkRx ?? -1) - (containerStats.get(b.id)?.networkRx ?? -1);
|
||||
break;
|
||||
case 'netTx':
|
||||
cmp = (containerStats.get(a.id)?.networkTx ?? -1) - (containerStats.get(b.id)?.networkTx ?? -1);
|
||||
break;
|
||||
}
|
||||
// Secondary sort by name for stability when primary values are equal
|
||||
if (cmp === 0 && sortField !== 'name') {
|
||||
@@ -1368,6 +1381,7 @@
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Handle tab visibility changes (e.g., user switches back from another tab)
|
||||
function handleVisibilityChange() {
|
||||
if (document.visibilityState === 'visible' && envId) {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||
import * as Popover from '$lib/components/ui/popover';
|
||||
import MultiSelectFilter from '$lib/components/MultiSelectFilter.svelte';
|
||||
import { Play, Square, Trash2, Plus, ArrowBigDown, Search, Pencil, ExternalLink, GitBranch, RefreshCw, Loader2, FileCode, FileText, FileOutput, Box, RotateCcw, ScrollText, Terminal, Eye, Network, HardDrive, Heart, HeartPulse, HeartOff, ChevronsUpDown, ChevronsDownUp, Rocket, AlertTriangle, X, Layers, Pause, CircleDashed, Skull, FolderOpen, Variable, Clock, RotateCw, Import, Ship, Cable, LayoutPanelLeft, Rows3, GripVertical, Globe, CircleArrowUp, NotepadText, Tag } from 'lucide-svelte';
|
||||
import { Play, Square, Trash2, Plus, ArrowBigDown, Search, Pencil, ExternalLink, GitBranch, RefreshCw, Loader2, FileCode, FileText, FileOutput, Box, RotateCcw, ScrollText, Terminal, Eye, Network, HardDrive, Heart, HeartPulse, HeartOff, ChevronsUpDown, ChevronsDownUp, Rocket, AlertTriangle, X, Layers, Pause, CircleDashed, Skull, FolderOpen, Variable, Clock, RotateCw, Import, Ship, Cable, LayoutPanelLeft, Rows3, GripVertical, Globe, CircleArrowUp, NotepadText, Tag, Copy, Check } from 'lucide-svelte';
|
||||
import { formatPorts } from '$lib/utils/port-format';
|
||||
import { parseCustomUrl } from '$lib/utils/custom-url';
|
||||
import { extractTraefikUrls } from '$lib/utils/traefik-urls';
|
||||
@@ -53,9 +53,10 @@
|
||||
import { ErrorDialog } from '$lib/components/ui/error-dialog';
|
||||
import { formatHostPortUrl } from '$lib/utils/url';
|
||||
import { formatBytes, formatBytesCompact } from '$lib/utils/format';
|
||||
import { copyToClipboard } from '$lib/utils/clipboard';
|
||||
import { effectiveStackBranch } from '$lib/git-stack-branch';
|
||||
|
||||
type SortField = 'name' | 'containers' | 'status' | 'cpu' | 'memory';
|
||||
type SortField = 'name' | 'containers' | 'status' | 'cpu' | 'memory' | 'diskRead' | 'diskWrite' | 'netRx' | 'netTx';
|
||||
type SortDirection = 'asc' | 'desc';
|
||||
|
||||
let stacks = $state<ComposeStackInfo[]>([]);
|
||||
@@ -66,8 +67,23 @@
|
||||
let stackSources = $state<Record<string, { sourceType: string; composePath?: string | null; repository?: any; gitStack?: any; icon?: string | null }>>({});
|
||||
let stackEnvVarCounts = $state<Record<string, number>>({});
|
||||
let gitStacks = $state<any[]>([]);
|
||||
let copiedWebhookStackId = $state<number | null>(null);
|
||||
|
||||
function copyWebhookUrl(stackId: number) {
|
||||
const url = `${window.location.origin}/api/git/stacks/${stackId}/webhook`;
|
||||
copyToClipboard(url).then((ok) => {
|
||||
if (!ok) return;
|
||||
copiedWebhookStackId = stackId;
|
||||
setTimeout(() => {
|
||||
if (copiedWebhookStackId === stackId) copiedWebhookStackId = null;
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
let gitRepositories = $state<any[]>([]);
|
||||
let gitCredentials = $state<any[]>([]);
|
||||
// User-set per-container icon overrides for the current env (name -> icon), so a
|
||||
// stack's expanded container list shows the same custom icons as the containers page (#1471).
|
||||
let iconOverrides = $state<Record<string, string>>({});
|
||||
let containerStats = $state<Map<string, ContainerStats>>(new Map());
|
||||
let containerStatsHistory = $state<Map<string, { cpu: number[]; mem: number[]; netRx: number[]; netTx: number[]; diskR: number[]; diskW: number[] }>>(new Map());
|
||||
let statsUpdateCount = $state(0); // Force reactivity counter
|
||||
@@ -593,6 +609,18 @@
|
||||
const memB = getStackStats(b)?.memoryUsage ?? -1;
|
||||
cmp = memA - memB;
|
||||
break;
|
||||
case 'diskRead':
|
||||
cmp = (getStackStats(a)?.blockRead ?? -1) - (getStackStats(b)?.blockRead ?? -1);
|
||||
break;
|
||||
case 'diskWrite':
|
||||
cmp = (getStackStats(a)?.blockWrite ?? -1) - (getStackStats(b)?.blockWrite ?? -1);
|
||||
break;
|
||||
case 'netRx':
|
||||
cmp = (getStackStats(a)?.networkRx ?? -1) - (getStackStats(b)?.networkRx ?? -1);
|
||||
break;
|
||||
case 'netTx':
|
||||
cmp = (getStackStats(a)?.networkTx ?? -1) - (getStackStats(b)?.networkTx ?? -1);
|
||||
break;
|
||||
}
|
||||
// Secondary sort by name for stability when primary values are equal
|
||||
if (cmp === 0 && sortField !== 'name') {
|
||||
@@ -819,11 +847,13 @@
|
||||
loading = true;
|
||||
}
|
||||
try {
|
||||
const [stacksRes, sourcesRes, gitStacksRes] = await Promise.all([
|
||||
const [stacksRes, sourcesRes, gitStacksRes, iconsRes] = await Promise.all([
|
||||
fetch(appendEnvParam('/api/stacks', envId)),
|
||||
fetch(appendEnvParam('/api/stacks/sources', envId)),
|
||||
fetch(appendEnvParam('/api/git/stacks', envId))
|
||||
fetch(appendEnvParam('/api/git/stacks', envId)),
|
||||
fetch(appendEnvParam('/api/container-icons', envId))
|
||||
]);
|
||||
iconOverrides = iconsRes.ok ? await iconsRes.json() : {};
|
||||
|
||||
// Handle stale environment ID (e.g., after database reset)
|
||||
if (stacksRes.status === 404 && envId) {
|
||||
@@ -1830,6 +1860,31 @@
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{/if}
|
||||
{:else if column.id === 'webhook'}
|
||||
{#if source.sourceType === 'git' && source.gitStack?.webhookEnabled}
|
||||
{@const stackId = source.gitStack.id}
|
||||
{@const webhookUrl = `${window.location.origin}/api/git/stacks/${stackId}/webhook`}
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger class="w-full text-left">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-1 text-xs font-mono text-muted-foreground hover:text-foreground transition-colors cursor-pointer"
|
||||
onclick={(e) => { e.stopPropagation(); copyWebhookUrl(stackId); }}
|
||||
>
|
||||
<span>#{stackId}</span>
|
||||
{#if copiedWebhookStackId === stackId}
|
||||
<Check class="w-3 h-3 text-emerald-500" />
|
||||
{:else}
|
||||
<Copy class="w-3 h-3" />
|
||||
{/if}
|
||||
</button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content class="max-w-md">
|
||||
<p class="text-xs mb-1">Copy webhook URL</p>
|
||||
<code class="text-2xs text-muted-foreground break-all">{webhookUrl}</code>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{/if}
|
||||
{:else if column.id === 'location'}
|
||||
{#if source.composePath}
|
||||
{@const dirPath = source.composePath.replace(/\/[^/]+$/, '')}
|
||||
@@ -2183,8 +2238,19 @@
|
||||
{@const isLoading = containerActionLoading === container.id}
|
||||
<div class="p-3 rounded-lg bg-background border text-xs">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
{#if $appSettings.useSelfhstIcons}
|
||||
<ContainerIcon image={container.image} name={container.service || container.name} class="w-4 h-4" />
|
||||
{#if $appSettings.useSelfhstIcons || iconOverrides[container.name]}
|
||||
<!-- override + its custom-icon key use the real container.name; name=
|
||||
stays the service for better auto-match when there is no override. -->
|
||||
<ContainerIcon
|
||||
image={container.image}
|
||||
name={container.service || container.name}
|
||||
override={iconOverrides[container.name]}
|
||||
overrideKey={container.name}
|
||||
{envId}
|
||||
class="w-4 h-4"
|
||||
fallbackClass={container.state === 'running' ? 'text-emerald-500' : 'text-muted-foreground'}
|
||||
showFallbackWhenOff
|
||||
/>
|
||||
{:else}
|
||||
<Box class="w-4 h-4 shrink-0 {container.state === 'running' ? 'text-emerald-500' : 'text-muted-foreground'}" />
|
||||
{/if}
|
||||
@@ -2698,6 +2764,7 @@
|
||||
bind:open={showGitModal}
|
||||
gitStack={editingGitStack}
|
||||
environmentId={envId}
|
||||
icon={editingGitStack ? (stackSources[editingGitStack.stackName]?.icon ?? null) : null}
|
||||
repositories={gitRepositories}
|
||||
credentials={gitCredentials}
|
||||
onClose={() => {
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* nextIoSortState: a two-metric grid column (Disk I/O = read/write) cycles a header
|
||||
* click through all four (metric, direction) states rather than a plain asc/desc
|
||||
* toggle (#1111).
|
||||
*/
|
||||
import { describe, it, expect } from 'bun:test';
|
||||
import { nextIoSortState } from '../src/lib/utils/io-sort-cycle';
|
||||
|
||||
const DISK = [
|
||||
{ field: 'diskRead', direction: 'desc' as const },
|
||||
{ field: 'diskRead', direction: 'asc' as const },
|
||||
{ field: 'diskWrite', direction: 'desc' as const },
|
||||
{ field: 'diskWrite', direction: 'asc' as const }
|
||||
];
|
||||
|
||||
describe('nextIoSortState', () => {
|
||||
it('starts the cycle at read-desc when the column is not currently sorted', () => {
|
||||
expect(nextIoSortState(DISK, { field: 'name', direction: 'asc' })).toEqual(DISK[0]);
|
||||
});
|
||||
|
||||
it('walks read-desc -> read-asc -> write-desc -> write-asc', () => {
|
||||
expect(nextIoSortState(DISK, DISK[0])).toEqual(DISK[1]);
|
||||
expect(nextIoSortState(DISK, DISK[1])).toEqual(DISK[2]);
|
||||
expect(nextIoSortState(DISK, DISK[2])).toEqual(DISK[3]);
|
||||
});
|
||||
|
||||
it('wraps from the last state back to the first', () => {
|
||||
expect(nextIoSortState(DISK, DISK[3])).toEqual(DISK[0]);
|
||||
});
|
||||
|
||||
it('restarts the cycle when the current field is another cycle column', () => {
|
||||
// e.g. the user was sorting Net I/O, now clicks Disk I/O
|
||||
expect(nextIoSortState(DISK, { field: 'netRx', direction: 'asc' })).toEqual(DISK[0]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user