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
-
-
-
+
+
+



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);