diff --git a/ThingConnect.Pulse.Server/Models/ConfigurationDtos.cs b/ThingConnect.Pulse.Server/Models/ConfigurationDtos.cs
index f811ef9..649a545 100644
--- a/ThingConnect.Pulse.Server/Models/ConfigurationDtos.cs
+++ b/ThingConnect.Pulse.Server/Models/ConfigurationDtos.cs
@@ -1,8 +1,36 @@
using System.ComponentModel.DataAnnotations;
+using System.Text.RegularExpressions;
using ThingConnect.Pulse.Server.Data;
namespace ThingConnect.Pulse.Server.Models;
+///
+/// Custom validation attribute that allows null values but validates non-null values against a regex pattern.
+///
+public sealed class OptionalRegularExpressionAttribute : ValidationAttribute
+{
+ private readonly Regex _regex;
+
+ public OptionalRegularExpressionAttribute(string pattern)
+ {
+ _regex = new Regex(pattern, RegexOptions.Compiled);
+ }
+
+ public override bool IsValid(object? value)
+ {
+ // Allow null values (optional field)
+ if (value == null)
+ return true;
+
+ // Allow empty strings (optional field)
+ if (value is string str && string.IsNullOrEmpty(str))
+ return true;
+
+ // Validate non-empty strings against the pattern
+ return value is string stringValue && _regex.IsMatch(stringValue);
+ }
+}
+
public class ConfigurationValidationException : Exception
{
public ValidationErrorsDto ValidationErrors { get; }
@@ -91,7 +119,7 @@ public sealed class GroupSection
public string? ParentId { get; set; }
- [RegularExpression("^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$", ErrorMessage = "Color must be a valid hex color code")]
+ [OptionalRegularExpression("^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$", ErrorMessage = "Color must be a valid hex color code")]
public string? Color { get; set; }
public int? SortOrder { get; set; }
diff --git a/ThingConnect.Pulse.Server/Models/StatusDtos.cs b/ThingConnect.Pulse.Server/Models/StatusDtos.cs
index 11df3f8..2b18afe 100644
--- a/ThingConnect.Pulse.Server/Models/StatusDtos.cs
+++ b/ThingConnect.Pulse.Server/Models/StatusDtos.cs
@@ -37,6 +37,7 @@ public sealed class GroupDto
public string Name { get; set; } = default!;
public string? ParentId { get; set; }
public string? Color { get; set; }
+ public int? SortOrder { get; set; }
}
public sealed class PageMetaDto
diff --git a/ThingConnect.Pulse.Server/Services/EndpointService.cs b/ThingConnect.Pulse.Server/Services/EndpointService.cs
index 48bdd71..817dd1a 100644
--- a/ThingConnect.Pulse.Server/Services/EndpointService.cs
+++ b/ThingConnect.Pulse.Server/Services/EndpointService.cs
@@ -94,7 +94,8 @@ private EndpointDto MapToEndpointDto(Data.Endpoint endpoint)
Id = endpoint.Group.Id,
Name = endpoint.Group.Name,
ParentId = endpoint.Group.ParentId,
- Color = endpoint.Group.Color
+ Color = endpoint.Group.Color,
+ SortOrder = endpoint.Group.SortOrder
},
Type = endpoint.Type.ToString().ToLower(),
Host = endpoint.Host,
diff --git a/ThingConnect.Pulse.Server/Services/HistoryService.cs b/ThingConnect.Pulse.Server/Services/HistoryService.cs
index 6c1c224..d90f034 100644
--- a/ThingConnect.Pulse.Server/Services/HistoryService.cs
+++ b/ThingConnect.Pulse.Server/Services/HistoryService.cs
@@ -187,7 +187,8 @@ private EndpointDto MapToEndpointDto(Data.Endpoint endpoint)
Id = endpoint.Group.Id,
Name = endpoint.Group.Name,
ParentId = endpoint.Group.ParentId,
- Color = endpoint.Group.Color
+ Color = endpoint.Group.Color,
+ SortOrder = endpoint.Group.SortOrder
},
Type = endpoint.Type.ToString().ToLower(),
Host = endpoint.Host,
diff --git a/ThingConnect.Pulse.Server/Services/StatusService.cs b/ThingConnect.Pulse.Server/Services/StatusService.cs
index 3fade8c..e6a31a3 100644
--- a/ThingConnect.Pulse.Server/Services/StatusService.cs
+++ b/ThingConnect.Pulse.Server/Services/StatusService.cs
@@ -53,9 +53,10 @@ public async Task> GetLiveStatusAsync(string? group, str
// Get total count for pagination
int totalCount = await query.CountAsync();
- // Apply pagination
+ // Apply pagination with proper group sorting
List endpoints = await query
- .OrderBy(e => e.GroupId)
+ .OrderBy(e => e.Group.SortOrder ?? int.MaxValue)
+ .ThenBy(e => e.Group.Name)
.ThenBy(e => e.Name)
.ToListAsync();
@@ -119,7 +120,8 @@ public async Task> GetLiveStatusAsync(string? group, str
List groups = await _context.Groups
.AsNoTracking()
- .OrderBy(g => g.Name)
+ .OrderBy(g => g.SortOrder ?? int.MaxValue)
+ .ThenBy(g => g.Name)
.ToListAsync();
// Cache for 5 minutes since groups don't change frequently
@@ -248,7 +250,8 @@ private EndpointDto MapToEndpointDto(Data.Endpoint endpoint)
Id = endpoint.Group.Id,
Name = endpoint.Group.Name,
ParentId = endpoint.Group.ParentId,
- Color = endpoint.Group.Color
+ Color = endpoint.Group.Color,
+ SortOrder = endpoint.Group.SortOrder
},
Type = endpoint.Type.ToString().ToLower(),
Host = endpoint.Host,
diff --git a/ThingConnect.Pulse.Server/config.schema.json b/ThingConnect.Pulse.Server/config.schema.json
index b1ba274..2e00465 100644
--- a/ThingConnect.Pulse.Server/config.schema.json
+++ b/ThingConnect.Pulse.Server/config.schema.json
@@ -39,8 +39,8 @@
},
"name": { "type": "string", "minLength": 1 },
"parent_id": { "type": ["string", "null"] },
- "color": { "type": "string", "pattern": "^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$" },
- "sort_order": { "type": ["integer", "string"] }
+ "color": { "type": ["string", "null"], "pattern": "^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$" },
+ "sort_order": { "type": ["integer", "string", "null"] }
},
"additionalProperties": false
}
diff --git a/ThingConnect.Pulse.Tests/config.schema.json b/ThingConnect.Pulse.Tests/config.schema.json
index b1ba274..2e00465 100644
--- a/ThingConnect.Pulse.Tests/config.schema.json
+++ b/ThingConnect.Pulse.Tests/config.schema.json
@@ -39,8 +39,8 @@
},
"name": { "type": "string", "minLength": 1 },
"parent_id": { "type": ["string", "null"] },
- "color": { "type": "string", "pattern": "^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$" },
- "sort_order": { "type": ["integer", "string"] }
+ "color": { "type": ["string", "null"], "pattern": "^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$" },
+ "sort_order": { "type": ["integer", "string", "null"] }
},
"additionalProperties": false
}