From a741640858ccfaa7faed2433d51770896bfa39f0 Mon Sep 17 00:00:00 2001 From: Maksym Sadovnychyy Date: Thu, 20 Aug 2026 15:28:39 +0200 Subject: [PATCH] (bugfix): sort resource table ip columns as addresses --- CHANGELOG.md | 6 ++ README.md | 6 +- src/Directory.Build.props | 2 +- .../ResourceColumnSort.cs | 63 +++++++++++++++++++ .../ResourceTableTests.cs | 11 ++++ 5 files changed, 84 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f8392b..0462822 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ## [Unreleased] +## [0.2.1] - 2026-08-20 + +### Fixed + +- Resource table IP columns (**Cluster IP**, **External IP**) sort as addresses, not text (`10.1.1.2` before `10.1.1.10`). + ## [0.2.0] - 2026-08-20 ### Added diff --git a/README.md b/README.md index 69832f3..aac6023 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # MaksIT.ClusterConsole -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-50.9%25-yellowgreen) -![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-41.5%25-yellowgreen) -![Method Coverage](https://img.shields.io/badge/Method%20Coverage-56.7%25-yellowgreen) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-52.7%25-yellowgreen) +![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-43.9%25-yellowgreen) +![Method Coverage](https://img.shields.io/badge/Method%20Coverage-60.2%25-green) ![.NET](https://img.shields.io/badge/.NET-10-512BD4) ![License](https://img.shields.io/badge/License-Apache%202.0-blue) ![Platform](https://img.shields.io/badge/Platform-Windows%20%7C%20Linux-0078D6) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index ffc8ae1..7cbf0bc 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -3,7 +3,7 @@ latest enable enable - 0.2.0 + 0.2.1 MaksIT.ClusterConsole MaksIT.ClusterConsole diff --git a/src/MaksIT.ClusterConsole.Shared/ResourceColumnSort.cs b/src/MaksIT.ClusterConsole.Shared/ResourceColumnSort.cs index 53d4777..de1597b 100644 --- a/src/MaksIT.ClusterConsole.Shared/ResourceColumnSort.cs +++ b/src/MaksIT.ClusterConsole.Shared/ResourceColumnSort.cs @@ -1,3 +1,4 @@ +using System.Net; using System.Collections; using System.Globalization; using MaksIT.ClusterConsole.Client; @@ -48,6 +49,7 @@ public static class ResourceColumnSort { "Memory" => KubeQuantity.ToBytes(left).CompareTo(KubeQuantity.ToBytes(right)), "Replicas" or "Desired" or "Current" or "Min" or "Max" or "Port" => ParseInt(left).CompareTo(ParseInt(right)), + _ when IsIpHeader(header) => CompareIpList(left, right), _ => string.Compare(left, right, StringComparison.OrdinalIgnoreCase) }; } @@ -83,6 +85,67 @@ public static class ResourceColumnSort { return total; } + private static bool IsIpHeader(string header) => + header.Equals("IP", StringComparison.OrdinalIgnoreCase) + || header.EndsWith(" IP", StringComparison.OrdinalIgnoreCase); + + private static int CompareIpList(string left, string right) { + var leftParts = SplitIpCell(left); + var rightParts = SplitIpCell(right); + var count = Math.Min(leftParts.Length, rightParts.Length); + for (var i = 0; i < count; i++) { + var cmp = CompareIpToken(leftParts[i], rightParts[i]); + if (cmp != 0) + return cmp; + } + + return leftParts.Length.CompareTo(rightParts.Length); + } + + private static string[] SplitIpCell(string text) => + text.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + + private static int CompareIpToken(string left, string right) { + var leftMissing = IsMissingIp(left); + var rightMissing = IsMissingIp(right); + if (leftMissing && rightMissing) + return 0; + if (leftMissing) + return -1; + if (rightMissing) + return 1; + + if (IPAddress.TryParse(left, out var leftIp)) { + if (IPAddress.TryParse(right, out var rightIp)) + return CompareAddress(leftIp, rightIp); + + return -1; + } + + if (IPAddress.TryParse(right, out _)) + return 1; + + return string.Compare(left, right, StringComparison.OrdinalIgnoreCase); + } + + private static bool IsMissingIp(string text) => + text.Equals("None", StringComparison.OrdinalIgnoreCase) + || text.Equals("", StringComparison.OrdinalIgnoreCase); + + private static int CompareAddress(IPAddress left, IPAddress right) { + var family = left.AddressFamily.CompareTo(right.AddressFamily); + if (family != 0) + return family; + + Span leftBytes = stackalloc byte[16]; + Span rightBytes = stackalloc byte[16]; + if (!left.TryWriteBytes(leftBytes, out var leftLength) + || !right.TryWriteBytes(rightBytes, out var rightLength)) + return string.CompareOrdinal(left.ToString(), right.ToString()); + + return leftBytes[..leftLength].SequenceCompareTo(rightBytes[..rightLength]); + } + private static int CompareReady(string left, string right) { ParseReady(left, out var leftReady, out var leftTotal); ParseReady(right, out var rightReady, out var rightTotal); diff --git a/src/MaksIT.ClusterConsole.Tests/ResourceTableTests.cs b/src/MaksIT.ClusterConsole.Tests/ResourceTableTests.cs index 2d3ffcc..9e6939a 100644 --- a/src/MaksIT.ClusterConsole.Tests/ResourceTableTests.cs +++ b/src/MaksIT.ClusterConsole.Tests/ResourceTableTests.cs @@ -29,6 +29,17 @@ public class ResourceTableTests { Assert.True(ResourceColumnSort.Compare("Age", "2d", "5h") > 0); } + [Fact] + public void Ip_columns_sort_as_addresses_not_text() { + Assert.True(ResourceColumnSort.Compare("Cluster IP", "10.1.1.2", "10.1.1.10") < 0); + Assert.True(ResourceColumnSort.Compare("Cluster IP", "10.1.1.10", "10.1.1.2") > 0); + Assert.Equal(0, ResourceColumnSort.Compare("External IP", "192.168.0.1", "192.168.0.1")); + Assert.True(ResourceColumnSort.Compare("Cluster IP", "None", "10.0.0.1") < 0); + Assert.True(ResourceColumnSort.Compare("External IP", "10.0.0.2,10.0.0.10", "10.0.0.2,10.0.0.3") > 0); + Assert.True(ResourceColumnSort.Compare("Cluster IP", "127.0.0.1", "::1") < 0); + Assert.True(ResourceColumnSort.Compare("Cluster IP", "2001:db8::1", "2001:db8::10") < 0); + } + [Fact] public void Ready_and_restarts_sort_numerically() { Assert.True(ResourceColumnSort.Compare("Ready", "1/2", "2/2") < 0);