From 4ff858ae29f5e0ab8aae1812f199afb465d2810e Mon Sep 17 00:00:00 2001 From: lqlive Date: Wed, 3 Dec 2025 11:59:02 +0800 Subject: [PATCH 1/2] Add support for statistics and enablement in Cluster and Route components - Introduced 'Enabled' property in Cluster and Route models, allowing for better control over their states. - Updated ClusterRequest and RouteRequest to include the new 'Enabled' field. - Enhanced ClusterEdit and RouteEdit components to manage the 'Enabled' state through forms. - Integrated Statistics API endpoints in the Program.cs and added corresponding service registrations. - Refactored Dashboard component to display statistics for routes, clusters, and certificates, improving the overall user experience. - Updated various types and interfaces to accommodate the new statistics and enablement features. --- .../RateLimiter/RateLimiterPolicyUpdater.cs | 3 +- src/Sail.Core/Entities/Cluster.cs | 1 + src/Sail.Core/Entities/Route.cs | 2 + .../Retry/SailRetryPolicyProvider.cs | 33 +- src/Sail/Cluster/ClusterService.cs | 3 + src/Sail/Cluster/Models/ClusterRequest.cs | 1 + src/Sail/Cluster/Models/ClusterResponse.cs | 1 + src/Sail/Extensions/Extensions.cs | 1 + src/Sail/Program.cs | 2 + src/Sail/Route/Models/RouteRequest.cs | 2 + src/Sail/Route/Models/RouteResponse.cs | 2 + src/Sail/Route/RouteService.cs | 6 + .../Http/StatisticsHttpEndpointsBuilder.cs | 88 ++++ .../Statistics/Models/StatisticsResponse.cs | 20 + src/Sail/Statistics/StatisticsService.cs | 114 +++++ src/Sail/StatisticsApi.http | 18 + web/src/pages/Clusters/ClusterEdit.tsx | 9 + web/src/pages/Dashboard.tsx | 455 ++++++++++++------ web/src/services/statisticsService.ts | 73 +++ web/src/types/cluster.ts | 1 + web/src/types/index.ts | 2 +- web/src/types/statistics.ts | 22 + 22 files changed, 703 insertions(+), 156 deletions(-) create mode 100644 src/Sail/Statistics/Http/StatisticsHttpEndpointsBuilder.cs create mode 100644 src/Sail/Statistics/Models/StatisticsResponse.cs create mode 100644 src/Sail/Statistics/StatisticsService.cs create mode 100644 src/Sail/StatisticsApi.http create mode 100644 web/src/services/statisticsService.ts create mode 100644 web/src/types/statistics.ts diff --git a/src/Sail.Compass/RateLimiter/RateLimiterPolicyUpdater.cs b/src/Sail.Compass/RateLimiter/RateLimiterPolicyUpdater.cs index 165c2c1..a2978e0 100644 --- a/src/Sail.Compass/RateLimiter/RateLimiterPolicyUpdater.cs +++ b/src/Sail.Compass/RateLimiter/RateLimiterPolicyUpdater.cs @@ -100,5 +100,4 @@ public static void UpdateRateLimiterPoliciesFailed(ILogger logger, Exception exc _updateRateLimiterPoliciesFailed(logger, exception); } } -} - +} \ No newline at end of file diff --git a/src/Sail.Core/Entities/Cluster.cs b/src/Sail.Core/Entities/Cluster.cs index 158dcf5..8960c09 100644 --- a/src/Sail.Core/Entities/Cluster.cs +++ b/src/Sail.Core/Entities/Cluster.cs @@ -4,6 +4,7 @@ public class Cluster { public Guid Id { get; set; } public string? Name { get; set; } + public bool? Enabled { get; set; } = true; public string? ServiceName { get; set; } public ServiceDiscoveryType? ServiceDiscoveryType { get; set; } public string? LoadBalancingPolicy { get; set; } diff --git a/src/Sail.Core/Entities/Route.cs b/src/Sail.Core/Entities/Route.cs index a9915d2..47c46d4 100644 --- a/src/Sail.Core/Entities/Route.cs +++ b/src/Sail.Core/Entities/Route.cs @@ -5,12 +5,14 @@ public class Route public Guid Id { get; set; } public Guid? ClusterId { get; set; } public string Name { get; set; } = string.Empty; + public bool? Enabled { get; set; } = true; public RouteMatch Match { get; set; } = new(); public int Order { get; set; } public string? AuthorizationPolicy { get; set; } public string? RateLimiterPolicy { get; set; } public string? CorsPolicy { get; set; } public string? TimeoutPolicy { get; set; } + public string? RetryPolicy { get; set; } public TimeSpan? Timeout { get; set; } public long? MaxRequestBodySize { get; set; } public bool? HttpsRedirect { get; set; } diff --git a/src/Sail.Core/Retry/SailRetryPolicyProvider.cs b/src/Sail.Core/Retry/SailRetryPolicyProvider.cs index dff671d..2ede33e 100644 --- a/src/Sail.Core/Retry/SailRetryPolicyProvider.cs +++ b/src/Sail.Core/Retry/SailRetryPolicyProvider.cs @@ -15,23 +15,6 @@ public SailRetryPolicyProvider(ILogger logger) _logger = logger; } - public RetryPipelineWrapper? GetPolicy(string key) - { - if (string.IsNullOrEmpty(key)) - { - return null; - } - - if (_policies.TryGetValue(key, out var policy)) - { - Log.PolicyFound(_logger, key); - return policy; - } - - Log.PolicyNotFound(_logger, key); - return null; - } - public Task UpdateAsync(IReadOnlyList configs, CancellationToken cancellationToken) { var newPolicies = new Dictionary(StringComparer.OrdinalIgnoreCase); @@ -87,6 +70,22 @@ private ResiliencePipeline BuildResiliencePipeline(RetryPolicyConfig config) return new ResiliencePipelineBuilder().AddRetry(retryStrategyOptions).Build(); } + public RetryPipelineWrapper? GetPolicy(string key) + { + if (string.IsNullOrEmpty(key)) + { + return null; + } + + if (_policies.TryGetValue(key, out var policy)) + { + Log.PolicyFound(_logger, key); + return policy; + } + + Log.PolicyNotFound(_logger, key); + return null; + } private static class Log { diff --git a/src/Sail/Cluster/ClusterService.cs b/src/Sail/Cluster/ClusterService.cs index 87b78a1..7706ed9 100644 --- a/src/Sail/Cluster/ClusterService.cs +++ b/src/Sail/Cluster/ClusterService.cs @@ -39,6 +39,7 @@ public async Task> UpdateAsync(Guid id, ClusterRequest request, } cluster.Name = request.Name; + cluster.Enabled = request.Enabled; cluster.ServiceName = request.ServiceName; cluster.ServiceDiscoveryType = request.ServiceDiscoveryType; cluster.LoadBalancingPolicy = request.LoadBalancingPolicy; @@ -62,6 +63,7 @@ private ClusterEntity CreateClusterFromRequest(ClusterRequest request) var cluster = new ClusterEntity { Name = request.Name, + Enabled = request.Enabled, LoadBalancingPolicy = request.LoadBalancingPolicy, ServiceName = request.ServiceName, ServiceDiscoveryType = request.ServiceDiscoveryType, @@ -134,6 +136,7 @@ private ClusterResponse MapToCluster(ClusterEntity cluster) { Id = cluster.Id, Name = cluster.Name, + Enabled = cluster.Enabled ?? true, ServiceName = cluster.ServiceName, ServiceDiscoveryType = cluster.ServiceDiscoveryType, LoadBalancingPolicy = cluster.LoadBalancingPolicy, diff --git a/src/Sail/Cluster/Models/ClusterRequest.cs b/src/Sail/Cluster/Models/ClusterRequest.cs index 84d4bc8..9cd77d9 100644 --- a/src/Sail/Cluster/Models/ClusterRequest.cs +++ b/src/Sail/Cluster/Models/ClusterRequest.cs @@ -4,6 +4,7 @@ namespace Sail.Cluster.Models; public record ClusterRequest( string Name, + bool Enabled, string? ServiceName, ServiceDiscoveryType? ServiceDiscoveryType, HealthCheckRequest? HealthCheck, diff --git a/src/Sail/Cluster/Models/ClusterResponse.cs b/src/Sail/Cluster/Models/ClusterResponse.cs index 1f022c2..f17389e 100644 --- a/src/Sail/Cluster/Models/ClusterResponse.cs +++ b/src/Sail/Cluster/Models/ClusterResponse.cs @@ -6,6 +6,7 @@ public record ClusterResponse { public Guid Id { get; init; } public string? Name { get; init; } + public bool Enabled { get; init; } public string? ServiceName { get; init; } public ServiceDiscoveryType? ServiceDiscoveryType { get; init; } public string? LoadBalancingPolicy { get; init; } diff --git a/src/Sail/Extensions/Extensions.cs b/src/Sail/Extensions/Extensions.cs index 7e150d5..498a184 100644 --- a/src/Sail/Extensions/Extensions.cs +++ b/src/Sail/Extensions/Extensions.cs @@ -18,6 +18,7 @@ public static void AddApplicationServices(this IHostApplicationBuilder builder) services.TryAddScoped(); services.TryAddScoped(); services.TryAddScoped(); + services.TryAddScoped(); services.AddValidatorsFromAssemblyContaining(); diff --git a/src/Sail/Program.cs b/src/Sail/Program.cs index 263fdbf..e87db09 100644 --- a/src/Sail/Program.cs +++ b/src/Sail/Program.cs @@ -14,6 +14,7 @@ using Sail.Middleware.Http; using Sail.AuthenticationPolicy.Grpc; using Sail.AuthenticationPolicy.Http; +using Sail.Statistics.Http; var builder = WebApplication.CreateBuilder(args); @@ -46,6 +47,7 @@ endpoint.MapCertificateApiV1(); endpoint.MapMiddlewareApiV1(); endpoint.MapAuthenticationPolicyApi(); +endpoint.MapStatisticsApiV1(); app.UseDefaultOpenApi(); app.MapGrpcService(); diff --git a/src/Sail/Route/Models/RouteRequest.cs b/src/Sail/Route/Models/RouteRequest.cs index b070e8f..6f618cd 100644 --- a/src/Sail/Route/Models/RouteRequest.cs +++ b/src/Sail/Route/Models/RouteRequest.cs @@ -4,12 +4,14 @@ public record RouteRequest { public Guid? ClusterId { get; init; } public string Name { get; init; } + public bool Enabled { get; init; } = true; public RouteMatchRequest Match { get; init; } public int Order { get; init; } public string? AuthorizationPolicy { get; init; } public string? RateLimiterPolicy { get; init; } public string? CorsPolicy { get; init; } public string? TimeoutPolicy { get; init; } + public string? RetryPolicy { get; init; } public TimeSpan? Timeout { get; init; } public long? MaxRequestBodySize { get; init; } public bool? HttpsRedirect { get; init; } diff --git a/src/Sail/Route/Models/RouteResponse.cs b/src/Sail/Route/Models/RouteResponse.cs index bd54fd9..b6c81fe 100644 --- a/src/Sail/Route/Models/RouteResponse.cs +++ b/src/Sail/Route/Models/RouteResponse.cs @@ -5,12 +5,14 @@ public record RouteResponse public Guid Id { get; init; } public Guid? ClusterId { get; init; } public string Name { get; init; } + public bool Enabled { get; init; } public RouteMatchResponse Match { get; init; } public int Order { get; init; } public string? AuthorizationPolicy { get; init; } public string? RateLimiterPolicy { get; init; } public string? CorsPolicy { get; init; } public string? TimeoutPolicy { get; init; } + public string? RetryPolicy { get; init; } public TimeSpan? Timeout { get; init; } public long? MaxRequestBodySize { get; init; } public bool? HttpsRedirect { get; init; } diff --git a/src/Sail/Route/RouteService.cs b/src/Sail/Route/RouteService.cs index 150f115..9a86c68 100644 --- a/src/Sail/Route/RouteService.cs +++ b/src/Sail/Route/RouteService.cs @@ -75,12 +75,14 @@ public async Task> UpdateAsync(Guid id, RouteRequest request, } route.Name = request.Name; + route.Enabled = request.Enabled; route.ClusterId = request.ClusterId; route.Match = CreateRouteMatchFromRequest(request.Match); route.AuthorizationPolicy = request.AuthorizationPolicy; route.RateLimiterPolicy = request.RateLimiterPolicy; route.CorsPolicy = request.CorsPolicy; route.TimeoutPolicy = request.TimeoutPolicy; + route.RetryPolicy = request.RetryPolicy; route.Timeout = request.Timeout; route.MaxRequestBodySize = request.MaxRequestBodySize; route.HttpsRedirect = request.HttpsRedirect; @@ -102,6 +104,7 @@ private RouteEntity CreateRouteFromRequest(RouteRequest request) var route = new RouteEntity { Name = request.Name, + Enabled = request.Enabled, ClusterId = request.ClusterId, Match = CreateRouteMatchFromRequest(request.Match), Order = request.Order, @@ -109,6 +112,7 @@ private RouteEntity CreateRouteFromRequest(RouteRequest request) RateLimiterPolicy = request.RateLimiterPolicy, CorsPolicy = request.CorsPolicy, TimeoutPolicy = request.TimeoutPolicy, + RetryPolicy = request.RetryPolicy, Timeout = request.Timeout, MaxRequestBodySize = request.MaxRequestBodySize, HttpsRedirect = request.HttpsRedirect, @@ -158,6 +162,7 @@ private RouteResponse MapToRoute(RouteEntity route) Id = route.Id, ClusterId = route.ClusterId, Name = route.Name, + Enabled = route.Enabled ?? true, Match = new RouteMatchResponse { Path = route.Match.Path, @@ -184,6 +189,7 @@ private RouteResponse MapToRoute(RouteEntity route) RateLimiterPolicy = route.RateLimiterPolicy, CorsPolicy = route.CorsPolicy, TimeoutPolicy = route.TimeoutPolicy, + RetryPolicy = route.RetryPolicy, Timeout = route.Timeout, MaxRequestBodySize = route.MaxRequestBodySize, HttpsRedirect = route.HttpsRedirect, diff --git a/src/Sail/Statistics/Http/StatisticsHttpEndpointsBuilder.cs b/src/Sail/Statistics/Http/StatisticsHttpEndpointsBuilder.cs new file mode 100644 index 0000000..7462ee5 --- /dev/null +++ b/src/Sail/Statistics/Http/StatisticsHttpEndpointsBuilder.cs @@ -0,0 +1,88 @@ +using Microsoft.AspNetCore.Http.HttpResults; +using Sail.Statistics.Models; + +namespace Sail.Statistics.Http; + +public static class StatisticsHttpEndpointsBuilder +{ + public static RouteGroupBuilder MapStatisticsApiV1(this IEndpointRouteBuilder app) + { + var api = app.MapGroup("api/statistics").HasApiVersion(1.0); + + api.MapGet("/resources/routes", GetRouteStatistics); + api.MapGet("/resources/clusters", GetClusterStatistics); + api.MapGet("/resources/certificates", GetCertificateStatistics); + api.MapGet("/resources/middlewares", GetMiddlewareStatistics); + api.MapGet("/resources/authentication-policies", GetAuthenticationPolicyStatistics); + api.MapGet("/recent/routes", GetRecentRoutes); + api.MapGet("/recent/clusters", GetRecentClusters); + api.MapGet("/recent/certificates", GetRecentCertificates); + + return api; + } + + private static async Task> GetRecentRoutes( + StatisticsService service, + CancellationToken cancellationToken) + { + var items = await service.GetRecentRoutesAsync(cancellationToken); + return TypedResults.Ok(items); + } + + private static async Task> GetRecentClusters( + StatisticsService service, + CancellationToken cancellationToken) + { + var items = await service.GetRecentClustersAsync(cancellationToken); + return TypedResults.Ok(items); + } + + private static async Task> GetRecentCertificates( + StatisticsService service, + CancellationToken cancellationToken) + { + var items = await service.GetRecentCertificatesAsync(cancellationToken); + return TypedResults.Ok(items); + } + + private static async Task> GetRouteStatistics( + StatisticsService service, + CancellationToken cancellationToken) + { + var count = await service.GetRouteStatisticsAsync(cancellationToken); + return TypedResults.Ok(count); + } + + private static async Task> GetClusterStatistics( + StatisticsService service, + CancellationToken cancellationToken) + { + var count = await service.GetClusterStatisticsAsync(cancellationToken); + return TypedResults.Ok(count); + } + + private static async Task> GetCertificateStatistics( + StatisticsService service, + CancellationToken cancellationToken) + { + var count = await service.GetCertificateStatisticsAsync(cancellationToken); + return TypedResults.Ok(count); + } + + private static async Task> GetMiddlewareStatistics( + StatisticsService service, + CancellationToken cancellationToken) + { + var count = await service.GetMiddlewareStatisticsAsync(cancellationToken); + return TypedResults.Ok(count); + } + + private static async Task> GetAuthenticationPolicyStatistics( + StatisticsService service, + CancellationToken cancellationToken) + { + var count = await service.GetAuthenticationPolicyStatisticsAsync(cancellationToken); + return TypedResults.Ok(count); + } +} + diff --git a/src/Sail/Statistics/Models/StatisticsResponse.cs b/src/Sail/Statistics/Models/StatisticsResponse.cs new file mode 100644 index 0000000..107a7fe --- /dev/null +++ b/src/Sail/Statistics/Models/StatisticsResponse.cs @@ -0,0 +1,20 @@ +namespace Sail.Statistics.Models; + +public record ResourceCountResponse +{ + public int Total { get; init; } + public int Enabled { get; init; } +} + +public record RecentItemsResponse +{ + public List Items { get; init; } = []; +} + +public record RecentItem +{ + public Guid Id { get; init; } + public string Name { get; init; } = string.Empty; + public DateTimeOffset CreatedAt { get; init; } +} + diff --git a/src/Sail/Statistics/StatisticsService.cs b/src/Sail/Statistics/StatisticsService.cs new file mode 100644 index 0000000..4ca2b5a --- /dev/null +++ b/src/Sail/Statistics/StatisticsService.cs @@ -0,0 +1,114 @@ +using Sail.Core.Stores; +using Sail.Statistics.Models; + +namespace Sail.Statistics; + +public class StatisticsService( + IRouteStore routeStore, + IClusterStore clusterStore, + ICertificateStore certificateStore, + IMiddlewareStore middlewareStore, + IAuthenticationPolicyStore authPolicyStore) +{ + public async Task GetRouteStatisticsAsync(CancellationToken cancellationToken = default) + { + var routes = await routeStore.GetAsync(cancellationToken); + return new ResourceCountResponse + { + Total = routes.Count, + Enabled = routes.Count(r => r.Enabled ?? true) + }; + } + + public async Task GetClusterStatisticsAsync(CancellationToken cancellationToken = default) + { + var clusters = await clusterStore.GetAsync(cancellationToken); + return new ResourceCountResponse + { + Total = clusters.Count, + Enabled = clusters.Count(c => c.Enabled ?? true) + }; + } + + public async Task GetCertificateStatisticsAsync(CancellationToken cancellationToken = default) + { + var certificates = await certificateStore.GetAsync(cancellationToken); + return new ResourceCountResponse + { + Total = certificates.Count, + Enabled = certificates.Count + }; + } + + public async Task GetMiddlewareStatisticsAsync(CancellationToken cancellationToken = default) + { + var middlewares = await middlewareStore.GetAsync(cancellationToken); + return new ResourceCountResponse + { + Total = middlewares.Count, + Enabled = middlewares.Count(m => m.Enabled) + }; + } + + public async Task GetAuthenticationPolicyStatisticsAsync(CancellationToken cancellationToken = default) + { + var authPolicies = await authPolicyStore.GetAsync(cancellationToken); + return new ResourceCountResponse + { + Total = authPolicies.Count, + Enabled = authPolicies.Count(p => p.Enabled) + }; + } + + public async Task GetRecentRoutesAsync(CancellationToken cancellationToken = default) + { + var routes = await routeStore.GetAsync(cancellationToken); + var items = routes + .OrderByDescending(r => r.CreatedAt) + .Take(5) + .Select(r => new RecentItem + { + Id = r.Id, + Name = r.Name, + CreatedAt = r.CreatedAt + }) + .ToList(); + + return new RecentItemsResponse { Items = items }; + } + + public async Task GetRecentClustersAsync(CancellationToken cancellationToken = default) + { + var clusters = await clusterStore.GetAsync(cancellationToken); + var items = clusters + .OrderByDescending(c => c.CreatedAt) + .Take(5) + .Select(c => new RecentItem + { + Id = c.Id, + Name = c.Name, + CreatedAt = c.CreatedAt + }) + .ToList(); + + return new RecentItemsResponse { Items = items }; + } + + public async Task GetRecentCertificatesAsync(CancellationToken cancellationToken = default) + { + var certificates = await certificateStore.GetAsync(cancellationToken); + var items = certificates + .OrderByDescending(c => c.CreatedAt) + .Take(5) + .Select(c => new RecentItem + { + Id = c.Id, + Name = c.Name ?? $"Certificate-{c.Id}", + CreatedAt = c.CreatedAt + }) + .ToList(); + + return new RecentItemsResponse { Items = items }; + } +} + diff --git a/src/Sail/StatisticsApi.http b/src/Sail/StatisticsApi.http new file mode 100644 index 0000000..b809d2e --- /dev/null +++ b/src/Sail/StatisticsApi.http @@ -0,0 +1,18 @@ +@baseUrl = http://localhost:8100 +@apiVersion = 1.0‘ + +### Get All Statistics +GET {{baseUrl}}/api/statistics?api-version=1.0 + +### Get Resource Statistics +GET {{baseUrl}}/api/statistics/resources?api-version=1.0 + +### Get Recent Routes +GET {{baseUrl}}/api/statistics/recent/routes?api-version=1.0 + +### Get Recent Clusters +GET {{baseUrl}}/api/statistics/recent/clusters?api-version=1.0 + +### Get Recent Certificates +GET {{baseUrl}}/api/statistics/recent/certificates?api-version=1.0 + diff --git a/web/src/pages/Clusters/ClusterEdit.tsx b/web/src/pages/Clusters/ClusterEdit.tsx index 4cee869..a120b5c 100644 --- a/web/src/pages/Clusters/ClusterEdit.tsx +++ b/web/src/pages/Clusters/ClusterEdit.tsx @@ -15,6 +15,7 @@ const ClusterEdit: React.FC = () => { const [formData, setFormData] = useState({ name: '', + enabled: true, useServiceDiscovery: false, serviceName: '', serviceDiscoveryType: 'Consul' as 'Consul' | 'Dns', @@ -76,6 +77,7 @@ const ClusterEdit: React.FC = () => { setFormData({ name: cluster.name || '', + enabled: cluster.enabled !== false, useServiceDiscovery: !!(cluster as any).serviceName, serviceName: (cluster as any).serviceName || '', serviceDiscoveryType: (cluster as any).serviceDiscoveryType || 'Consul', @@ -121,6 +123,7 @@ const ClusterEdit: React.FC = () => { const clusterData: any = { name: formData.name, + enabled: formData.enabled, loadBalancingPolicy: formData.loadBalancingPolicy || undefined, serviceName: formData.useServiceDiscovery ? formData.serviceName : undefined, serviceDiscoveryType: formData.useServiceDiscovery ? (formData.serviceDiscoveryType === 'Consul' ? 1 : 2) : undefined, @@ -255,6 +258,12 @@ const ClusterEdit: React.FC = () => { /> + setFormData({ ...formData, enabled: checked })} + label="Enable this cluster immediately" + /> + { const [loading, setLoading] = useState(true); - const [stats, setStats] = useState(mockStats); + const [statistics, setStatistics] = useState(null); useEffect(() => { - setLoading(true); - setTimeout(() => { - setStats(mockStats); - setLoading(false); - }, 500); + loadAllStatistics(); }, []); - if (loading) { - return ( -
-
-
- ); - } + const loadAllStatistics = async () => { + try { + setLoading(true); + const data = await StatisticsService.getAllStatistics(); + setStatistics(data); + } catch (err) { + console.error('Failed to load statistics:', err); + } finally { + setLoading(false); + } + }; + + const formatDate = (dateString: string) => { + const date = new Date(dateString); + const now = new Date(); + const diffMs = now.getTime() - date.getTime(); + const diffMins = Math.floor(diffMs / 60000); + const diffHours = Math.floor(diffMs / 3600000); + const diffDays = Math.floor(diffMs / 86400000); + + if (diffMins < 1) return 'Just now'; + if (diffMins < 60) return `${diffMins}m ago`; + if (diffHours < 24) return `${diffHours}h ago`; + if (diffDays < 7) return `${diffDays}d ago`; + return date.toLocaleDateString(); + }; + return (
- {/* Header */} -
-

Dashboard

-

Gateway overview and statistics

+
+
+

Dashboard

+

Gateway overview and statistics

+
+
- {/* Stats Grid */} -
- {/* Routes Card */} -
-
-
- -
-
-
Routes
-
{stats.totalRoutes}
+
+ +
+
+
-
- - {stats.activeRoutes} - active routes +
+
Routes
+ {loading ? ( +
+
+
+ ) : statistics ? ( +
{statistics.routes.total}
+ ) : ( +
-
+ )}
-
+
+ {loading ? ( + Loading... + ) : statistics ? ( + <> +
+ {statistics.routes.enabled} + enabled + + ) : ( + - + )} +
+ - {/* Clusters Card */} -
-
-
- -
-
-
Clusters
-
{stats.totalClusters}
+ +
+
+
-
- - {stats.activeClusters} - active clusters +
+
Clusters
+ {loading ? ( +
+
+
+ ) : statistics ? ( +
{statistics.clusters.total}
+ ) : ( +
-
+ )}
-
+
+ {loading ? ( + Loading... + ) : statistics ? ( + <> +
+ {statistics.clusters.enabled} + enabled + + ) : ( + - + )} +
+ - {/* Certificates Card */} -
-
-
- + +
+
+
-
-
Certificates
-
{stats.totalCertificates}
+
+
+
Certificates
+ {loading ? ( +
+
+
+ ) : statistics ? ( +
{statistics.certificates.total}
+ ) : ( +
-
+ )} +
+
+ {loading ? ( + Loading... + ) : ( + <> +
+ SSL/TLS + + )} +
+ + + +
+
+
-
- {stats.expiringCertificates > 0 ? ( +
+
Middlewares
+ {loading ? ( +
+
+
+ ) : statistics ? ( +
{statistics.middlewares.total}
+ ) : ( +
-
+ )} +
+
+ {loading ? ( + Loading... + ) : statistics ? ( <> - - {stats.expiringCertificates} - expiring soon +
+ {statistics.middlewares.enabled} + enabled ) : ( + - + )} +
+ + + +
+
+ +
+
+
+
Auth Policies
+ {loading ? ( +
+
+
+ ) : statistics ? ( +
{statistics.authenticationPolicies.total}
+ ) : ( +
-
+ )} +
+
+ {loading ? ( + Loading... + ) : statistics ? ( <> - - All certificates valid +
+ {statistics.authenticationPolicies.enabled} + enabled + ) : ( + - )}
-
+
- {/* Detail Cards */} -
- {/* Destination Health Card */} -
-

Destination Health

-
-
- Total Destinations - {stats.totalDestinations} -
-
- Healthy - {stats.healthyDestinations} -
-
- Unhealthy - - {stats.totalDestinations - stats.healthyDestinations} - -
- - {/* Progress Bar */} -
-
- Health Rate - - {Math.round((stats.healthyDestinations / stats.totalDestinations) * 100)}% - -
-
-
-
+
+
+
+

Recent Routes

+
+
+ {loading ? ( +
+
+
+ ) : statistics && statistics.recentRoutes.length > 0 ? ( +
+ {statistics.recentRoutes.map((route) => ( + +
+

{route.name}

+

{formatDate(route.createdAt)}

+
+
+ + + +
+ + ))} +
+ ) : ( +
+
+ +
+

No routes yet

+
+ )}
- {/* System Status Card */} -
-

System Status

-
-
- Active Routes -
- {stats.activeRoutes}/{stats.totalRoutes} -
-
+
+
+

Recent Clusters

+
+
-
- Active Clusters -
- {stats.activeClusters}/{stats.totalClusters} -
-
+
+ {loading ? ( +
+
-
- Valid Certificates -
- {stats.validCertificates}/{stats.totalCertificates} -
0 ? 'bg-yellow-500' : 'bg-green-500'}`}>
+ ) : statistics && statistics.recentClusters.length > 0 ? ( +
+ {statistics.recentClusters.map((cluster) => ( + +
+

{cluster.name}

+

{formatDate(cluster.createdAt)}

+
+
+ + + +
+ + ))} +
+ ) : ( +
+
+
+

No clusters yet

+
+ )} +
+ +
+
+

Recent Certificates

+
+
+ {loading ? ( +
+
+
+ ) : statistics && statistics.recentCertificates.length > 0 ? ( +
+ {statistics.recentCertificates.map((cert) => ( + +
+

{cert.name}

+

{formatDate(cert.createdAt)}

+
+
+ + + +
+ + ))} +
+ ) : ( +
+
+ +
+

No certificates yet

+
+ )}
diff --git a/web/src/services/statisticsService.ts b/web/src/services/statisticsService.ts new file mode 100644 index 0000000..db134dc --- /dev/null +++ b/web/src/services/statisticsService.ts @@ -0,0 +1,73 @@ +import { apiClient } from './api'; +import type { RecentItem, ResourceCount, DashboardStatistics } from '../types/statistics'; + +export const StatisticsService = { + async getAllStatistics(): Promise { + const [ + routes, + clusters, + certificates, + middlewares, + authenticationPolicies, + recentRoutes, + recentClusters, + recentCertificates, + ] = await Promise.allSettled([ + this.getRouteStatistics(), + this.getClusterStatistics(), + this.getCertificateStatistics(), + this.getMiddlewareStatistics(), + this.getAuthenticationPolicyStatistics(), + this.getRecentRoutes(), + this.getRecentClusters(), + this.getRecentCertificates(), + ]); + + return { + routes: routes.status === 'fulfilled' ? routes.value : { total: 0, enabled: 0 }, + clusters: clusters.status === 'fulfilled' ? clusters.value : { total: 0, enabled: 0 }, + certificates: certificates.status === 'fulfilled' ? certificates.value : { total: 0, enabled: 0 }, + middlewares: middlewares.status === 'fulfilled' ? middlewares.value : { total: 0, enabled: 0 }, + authenticationPolicies: authenticationPolicies.status === 'fulfilled' ? authenticationPolicies.value : { total: 0, enabled: 0 }, + recentRoutes: recentRoutes.status === 'fulfilled' ? recentRoutes.value : [], + recentClusters: recentClusters.status === 'fulfilled' ? recentClusters.value : [], + recentCertificates: recentCertificates.status === 'fulfilled' ? recentCertificates.value : [], + }; + }, + + async getRouteStatistics(): Promise { + return await apiClient.get('/api/statistics/resources/routes'); + }, + + async getClusterStatistics(): Promise { + return await apiClient.get('/api/statistics/resources/clusters'); + }, + + async getCertificateStatistics(): Promise { + return await apiClient.get('/api/statistics/resources/certificates'); + }, + + async getMiddlewareStatistics(): Promise { + return await apiClient.get('/api/statistics/resources/middlewares'); + }, + + async getAuthenticationPolicyStatistics(): Promise { + return await apiClient.get('/api/statistics/resources/authentication-policies'); + }, + + async getRecentRoutes(): Promise { + const response = await apiClient.get<{ items: RecentItem[] }>('/api/statistics/recent/routes'); + return response.items; + }, + + async getRecentClusters(): Promise { + const response = await apiClient.get<{ items: RecentItem[] }>('/api/statistics/recent/clusters'); + return response.items; + }, + + async getRecentCertificates(): Promise { + const response = await apiClient.get<{ items: RecentItem[] }>('/api/statistics/recent/certificates'); + return response.items; + }, +}; + diff --git a/web/src/types/cluster.ts b/web/src/types/cluster.ts index d16eee2..a7290e6 100644 --- a/web/src/types/cluster.ts +++ b/web/src/types/cluster.ts @@ -26,6 +26,7 @@ export interface ServiceDiscovery { export interface Cluster { id: string; name: string; + enabled: boolean; serviceName?: string; serviceDiscoveryType?: ServiceDiscoveryType; loadBalancingPolicy?: string; diff --git a/web/src/types/index.ts b/web/src/types/index.ts index 792a804..a50eda8 100644 --- a/web/src/types/index.ts +++ b/web/src/types/index.ts @@ -1,8 +1,8 @@ -// Export all types from individual modules export * from './route' export * from './cluster' export * from './certificate' export * from './middleware' export * from './authentication' export * from './stats' +export * from './statistics' diff --git a/web/src/types/statistics.ts b/web/src/types/statistics.ts new file mode 100644 index 0000000..48fa5e4 --- /dev/null +++ b/web/src/types/statistics.ts @@ -0,0 +1,22 @@ +export interface ResourceCount { + total: number; + enabled: number; +} + +export interface RecentItem { + id: string; + name: string; + createdAt: string; +} + +export interface DashboardStatistics { + routes: ResourceCount; + clusters: ResourceCount; + certificates: ResourceCount; + middlewares: ResourceCount; + authenticationPolicies: ResourceCount; + recentRoutes: RecentItem[]; + recentClusters: RecentItem[]; + recentCertificates: RecentItem[]; +} + From ea843b7bc07413d09f9b402741e78b9acad3b443 Mon Sep 17 00:00:00 2001 From: lqlive Date: Wed, 3 Dec 2025 12:05:19 +0800 Subject: [PATCH 2/2] Update Certificates, Clusters, and Routes components to display enablement status with visual indicators. Added "Configured" label for Certificates and "Enabled/Disabled" status for Clusters and Routes, enhancing user clarity on component states. --- web/src/pages/Certificates/index.tsx | 4 +++- web/src/pages/Clusters/index.tsx | 7 ++++++- web/src/pages/Routes/index.tsx | 17 +++++------------ 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/web/src/pages/Certificates/index.tsx b/web/src/pages/Certificates/index.tsx index 6690da5..f0f2fc1 100644 --- a/web/src/pages/Certificates/index.tsx +++ b/web/src/pages/Certificates/index.tsx @@ -167,7 +167,9 @@ const Certificates: React.FC = () => {

{cert.name || `Certificate ${cert.id.substring(0, 8)}`}

-

ID: {cert.id.substring(0, 8)}...

+
+ ● Configured +
diff --git a/web/src/pages/Clusters/index.tsx b/web/src/pages/Clusters/index.tsx index d53058a..6e9184d 100644 --- a/web/src/pages/Clusters/index.tsx +++ b/web/src/pages/Clusters/index.tsx @@ -173,13 +173,18 @@ const Clusters: React.FC = () => {

{cluster.name}

-
+
{hasServiceDiscovery && ( Service Discovery )} + {cluster.enabled ? ( + ● Enabled + ) : ( + ○ Disabled + )}
diff --git a/web/src/pages/Routes/index.tsx b/web/src/pages/Routes/index.tsx index b08eea2..40349a5 100644 --- a/web/src/pages/Routes/index.tsx +++ b/web/src/pages/Routes/index.tsx @@ -4,7 +4,6 @@ import { MagnifyingGlassIcon, PlusIcon, CheckCircleIcon, - XCircleIcon, TrashIcon, } from '@heroicons/react/24/outline'; import type { Route } from '../../types'; @@ -198,20 +197,14 @@ const Routes: React.FC = () => {

{route.name}

- {route.enabled ? ( - - - Active - - ) : ( - - - Inactive - - )} #{route.order} + {route.enabled ? ( + ● Enabled + ) : ( + ○ Disabled + )}