Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion ThingConnect.Pulse.Server/Models/ConfigurationDtos.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,36 @@
using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions;
using ThingConnect.Pulse.Server.Data;

namespace ThingConnect.Pulse.Server.Models;

/// <summary>
/// Custom validation attribute that allows null values but validates non-null values against a regex pattern.
/// </summary>
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; }
Expand Down Expand Up @@ -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; }
Expand Down
1 change: 1 addition & 0 deletions ThingConnect.Pulse.Server/Models/StatusDtos.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion ThingConnect.Pulse.Server/Services/EndpointService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion ThingConnect.Pulse.Server/Services/HistoryService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
11 changes: 7 additions & 4 deletions ThingConnect.Pulse.Server/Services/StatusService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,10 @@ public async Task<List<LiveStatusItemDto>> GetLiveStatusAsync(string? group, str
// Get total count for pagination
int totalCount = await query.CountAsync();

// Apply pagination
// Apply pagination with proper group sorting
List<Data.Endpoint> endpoints = await query
.OrderBy(e => e.GroupId)
.OrderBy(e => e.Group.SortOrder ?? int.MaxValue)
.ThenBy(e => e.Group.Name)
.ThenBy(e => e.Name)
.ToListAsync();

Expand Down Expand Up @@ -119,7 +120,8 @@ public async Task<List<LiveStatusItemDto>> GetLiveStatusAsync(string? group, str

List<Group> 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
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions ThingConnect.Pulse.Server/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
4 changes: 2 additions & 2 deletions ThingConnect.Pulse.Tests/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Loading