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

+15 -1
View File
@@ -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) {
+73 -6
View File
@@ -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={() => {