Browse Source

Added filtering & sorting to the label browser (#229)

Tim Jones 1 month ago
parent
commit
4fac2b1dc4
1 changed files with 148 additions and 8 deletions
  1. 148 8
      Shared.Rcl/Components/LabelPage.razor

+ 148 - 8
Shared.Rcl/Components/LabelPage.razor

@@ -16,6 +16,34 @@
         </h1>
     </div>
 
+    <!-- Controls -->
+    @if (_resources is not null && _resources.Count > 0)
+    {
+        <div class="flex flex-wrap gap-4 items-center text-xs">
+
+            <!-- Filter -->
+            <input placeholder="filter resources..."
+                   class="bg-zinc-900 border border-zinc-800 px-3 py-1 rounded focus:outline-none focus:border-emerald-600"
+                   @bind="_filter" />
+
+            <!-- Sort By -->
+            <select class="bg-zinc-900 border border-zinc-800 px-2 py-1 rounded"
+                    @bind="_sortBy">
+                <option value="Value">Value</option>
+                <option value="Name">Name</option>
+                <option value="Kind">Kind</option>
+            </select>
+
+            <!-- Direction -->
+            <button class="border border-zinc-800 px-3 py-1 rounded hover:border-emerald-600"
+                    @onclick="ToggleSortDirection">
+                @(_ascending ? "Asc ↑" : "Desc ↓")
+            </button>
+
+        </div>
+    }
+
+    <!-- Content -->
     @if (_resources is null)
     {
         <div class="text-zinc-500">loading resources…</div>
@@ -24,11 +52,18 @@
     {
         <div class="text-zinc-500">no resources found for this label</div>
     }
+    else if (!GetProcessed().Any())
+    {
+        <div class="text-zinc-500">no results match your filter</div>
+    }
     else
     {
         <div class="space-y-3">
-            @foreach (var (resource, value) in _resources.OrderBy(r => r.Item1.Name))
+            @foreach (var item in GetProcessed())
             {
+                var resource = item.Resource;
+                var value = item.Value;
+
                 <div class="border border-zinc-800 rounded p-3 bg-zinc-900 hover:border-emerald-700 transition">
                     <NavLink href="@GetResourceUrl(resource)"
                              class="block hover:text-emerald-300">
@@ -44,8 +79,14 @@
                         </div>
 
                         <!-- Label value -->
-                        <div class="mt-2 text-xs text-emerald-400">
-                            Value: @value
+                        <div class="mt-2 text-xs text-emerald-400 flex items-center gap-2">
+                            <span>
+                                Value: @FormatValue(value)
+                            </span>
+
+                            <span class="text-zinc-500 uppercase text-[10px] tracking-wide">
+                                [@DetectType(value)]
+                            </span>
                         </div>
 
                     </NavLink>
@@ -59,26 +100,125 @@
 @code {
     [Parameter] public string LabelName { get; set; } = string.Empty;
 
-    private IReadOnlyList<(Resource, string)>? _resources;
+    private IReadOnlyList<(Resource Resource, string Value)>? _resources;
+
+    private string _sortBy = "Value";
+    private bool _ascending = true;
+    private string _filter = string.Empty;
 
     protected override async Task OnParametersSetAsync()
     {
         var decoded = Uri.UnescapeDataString(LabelName);
         _resources = await ResourceRepository.GetByLabelAsync(decoded);
     }
+    
+    private IEnumerable<(Resource Resource, string Value)> GetProcessed()
+    {
+        if (_resources is null)
+            return [];
+
+        var query = _resources.AsEnumerable();
+
+        // Filtering
+        if (!string.IsNullOrWhiteSpace(_filter))
+        {
+            query = query.Where(r =>
+                r.Resource.Name.Contains(_filter, StringComparison.OrdinalIgnoreCase) ||
+                r.Value.Contains(_filter, StringComparison.OrdinalIgnoreCase));
+        }
+
+        // Sorting
+        query = _sortBy switch
+        {
+            "Value" => query
+                .OrderBy(r => GetSortKey(r.Value))
+                .ThenBy(r => r.Resource.Name),
+            "Kind"  => query.OrderBy(r => r.Resource.Kind),
+            _       => query.OrderBy(r => r.Resource.Name)
+        };
+
+        if (!_ascending)
+            query = query.Reverse();
+
+        return query.ToList();
+    }
+
+    private void ToggleSortDirection()
+    {
+        _ascending = !_ascending;
+    }
 
+    private static string DetectType(string value)
+    {
+        if (DateTime.TryParse(value, out _)) return "DATE";
+        if (bool.TryParse(value, out _)) return "BOOL";
+        if (decimal.TryParse(value, out _)) return "NUMBER";
+        return "TEXT";
+    }
+
+    private static string FormatValue(string value)
+    {
+        if (DateTime.TryParse(value, out var dt))
+            return dt.ToString("yyyy-MM-dd");
+
+        return value;
+    }
+    
     private string GetResourceUrl(Resource resource)
     {
         if (resource.Kind == SystemResource.KindLabel)
-        {
             return $"resources/systems/{Uri.EscapeDataString(resource.Name)}";
-        }
 
         if (resource.Kind == Service.KindLabel)
-        {
             return $"resources/services/{Uri.EscapeDataString(resource.Name)}";
-        }
 
         return $"resources/hardware/{Uri.EscapeDataString(resource.Name)}";
     }
+    
+    private readonly record struct SortKey(int TypeRank, object Value, string Fallback)
+        : IComparable<SortKey>
+    {
+        public int CompareTo(SortKey other)
+        {
+            var rank = TypeRank.CompareTo(other.TypeRank);
+            if (rank != 0) return rank;
+            
+            var valueCompare = TypeRank switch
+            {
+                0 => ((int)Value).CompareTo((int)other.Value),   
+                1 => ((DateTime)Value).CompareTo((DateTime)other.Value),  
+                2 => ((decimal)Value).CompareTo((decimal)other.Value),   
+                3 => ((bool)Value).CompareTo((bool)other.Value),      
+                _ => string.Compare((string)Value, (string)other.Value, StringComparison.Ordinal)
+            };
+
+            if (valueCompare != 0) return valueCompare;
+
+            // Deterministic tie-breaker
+            return string.Compare(Fallback, other.Fallback, StringComparison.Ordinal);
+        }
+    }
+
+    private static SortKey GetSortKey(string? raw)
+    {
+        var s = raw?.Trim() ?? string.Empty;
+
+        if (string.IsNullOrEmpty(s))
+            return new SortKey(TypeRank: 0, Value: 0, Fallback: "");
+
+        // Date
+        if (DateTime.TryParse(s, out var dt))
+            return new SortKey(TypeRank: 1, Value: dt, Fallback: s);
+
+        // Number
+        if (decimal.TryParse(s, out var dec))
+            return new SortKey(TypeRank: 2, Value: dec, Fallback: s);
+
+        // Bool
+        if (bool.TryParse(s, out var b))
+            return new SortKey(TypeRank: 3, Value: b, Fallback: s);
+
+        // Text (case-insensitive sort by normalizing)
+        return new SortKey(TypeRank: 4, Value: s.ToLowerInvariant(), Fallback: s);
+    }
 }