From 91afb0dd8e82216399a3a1ee760e95fdafdaf01f Mon Sep 17 00:00:00 2001 From: Henry Hobbs Date: Sat, 17 May 2025 22:26:58 -0400 Subject: [PATCH 1/3] Added ability to update config via API at runtime. wip on customizing config via UI --- .../Components/Pages/Home.razor | 4 +- .../Models/AccessQueueConfig.cs | 10 ----- .../Services/AccessQueueManager.cs | 10 ++--- .../Services/IAccessQueueManager.cs | 2 + .../Controllers/AccessController.cs | 13 ++++++ .../Models/AccessQueueConfig.cs | 15 +++++++ AccessQueueService/Services/AccessService.cs | 41 +++++++++++++------ AccessQueueService/Services/IAccessService.cs | 3 ++ 8 files changed, 69 insertions(+), 29 deletions(-) delete mode 100644 AccessQueuePlayground/Models/AccessQueueConfig.cs create mode 100644 AccessQueueService/Models/AccessQueueConfig.cs diff --git a/AccessQueuePlayground/Components/Pages/Home.razor b/AccessQueuePlayground/Components/Pages/Home.razor index fbfe951..3bc5f6e 100644 --- a/AccessQueuePlayground/Components/Pages/Home.razor +++ b/AccessQueuePlayground/Components/Pages/Home.razor @@ -1,6 +1,7 @@ @page "/" @using AccessQueuePlayground.Models @using AccessQueuePlayground.Services +@using AccessQueueService.Models; @using BlazorBootstrap @inject IAccessQueueManager Manager @@ -12,7 +13,8 @@

Expiration Seconds: @Config.ExpirationSeconds, Activity Seconds: @Config.ActivitySeconds, - Capacity Limit: @Config.CapacityLimit + Capacity Limit: @Config.CapacityLimit, + Rolling Expiration: @Config.RollingExpiration

}

diff --git a/AccessQueuePlayground/Models/AccessQueueConfig.cs b/AccessQueuePlayground/Models/AccessQueueConfig.cs deleted file mode 100644 index 3bf62ef..0000000 --- a/AccessQueuePlayground/Models/AccessQueueConfig.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace AccessQueuePlayground.Models -{ - public class AccessQueueConfig - { - public int ActivitySeconds { get; set; } - public int ExpirationSeconds { get; set; } - public int CapacityLimit { get; set; } - - } -} diff --git a/AccessQueuePlayground/Services/AccessQueueManager.cs b/AccessQueuePlayground/Services/AccessQueueManager.cs index d3237bb..5bad7e2 100644 --- a/AccessQueuePlayground/Services/AccessQueueManager.cs +++ b/AccessQueuePlayground/Services/AccessQueueManager.cs @@ -29,12 +29,12 @@ namespace AccessQueuePlayground.Services public AccessQueueStatus GetStatus() => _status; - public AccessQueueConfig GetConfig() => new AccessQueueConfig + public AccessQueueConfig GetConfig() => _accessService.GetConfiguration(); + + public void UpdateConfig(AccessQueueConfig config) { - ActivitySeconds = _config.GetValue("AccessQueue:ActivitySeconds"), - ExpirationSeconds = _config.GetValue("AccessQueue:ExpirationSeconds"), - CapacityLimit = _config.GetValue("AccessQueue:CapacityLimit") - }; + _accessService.UpdateConfiguration(config); + } public Guid AddUser() { diff --git a/AccessQueuePlayground/Services/IAccessQueueManager.cs b/AccessQueuePlayground/Services/IAccessQueueManager.cs index 5c16ff1..015dbd8 100644 --- a/AccessQueuePlayground/Services/IAccessQueueManager.cs +++ b/AccessQueuePlayground/Services/IAccessQueueManager.cs @@ -1,4 +1,5 @@ using AccessQueuePlayground.Models; +using AccessQueueService.Models; namespace AccessQueuePlayground.Services { @@ -6,6 +7,7 @@ namespace AccessQueuePlayground.Services { public event Action? StatusUpdated; public AccessQueueConfig GetConfig(); + public void UpdateConfig(AccessQueueConfig config); public Task RecalculateStatus(); public AccessQueueStatus GetStatus(); public Guid AddUser(); diff --git a/AccessQueueService/Controllers/AccessController.cs b/AccessQueueService/Controllers/AccessController.cs index f9fcc05..d1f4786 100644 --- a/AccessQueueService/Controllers/AccessController.cs +++ b/AccessQueueService/Controllers/AccessController.cs @@ -28,5 +28,18 @@ namespace AccessQueueService.Controllers { return await _accessService.RevokeAccess(id); } + + [HttpGet("configuration")] + public ActionResult GetConfiguration() + { + return Ok(_accessService.GetConfiguration()); + } + + [HttpPost("configuration")] + public IActionResult UpdateConfiguration([FromBody] AccessQueueConfig config) + { + _accessService.PatchConfiguration(config); + return NoContent(); + } } } diff --git a/AccessQueueService/Models/AccessQueueConfig.cs b/AccessQueueService/Models/AccessQueueConfig.cs new file mode 100644 index 0000000..413caca --- /dev/null +++ b/AccessQueueService/Models/AccessQueueConfig.cs @@ -0,0 +1,15 @@ +namespace AccessQueueService.Models +{ + public class AccessQueueConfig + { + public int? CapacityLimit { get; set; } + public int? ActivitySeconds { get; set; } + public int? ExpirationSeconds { get; set; } + public bool? RollingExpiration { get; set; } + + public AccessQueueConfig Clone() + { + return (AccessQueueConfig)this.MemberwiseClone(); + } + } +} diff --git a/AccessQueueService/Services/AccessService.cs b/AccessQueueService/Services/AccessService.cs index 65d0f9a..891ab88 100644 --- a/AccessQueueService/Services/AccessService.cs +++ b/AccessQueueService/Services/AccessService.cs @@ -12,37 +12,52 @@ namespace AccessQueueService.Services private readonly ILogger _logger; private readonly SemaphoreSlim _queueLock = new(1, 1); - private readonly int EXP_SECONDS; - private readonly int ACT_SECONDS; - private readonly int CAPACITY_LIMIT; - private readonly bool ROLLING_EXPIRATION; + private AccessQueueConfig _config; public AccessService(IConfiguration configuration, IAccessQueueRepo accessQueueRepo, ILogger logger) { _configuration = configuration; _accessQueueRepo = accessQueueRepo; _logger = logger; - EXP_SECONDS = _configuration.GetValue("AccessQueue:ExpirationSeconds"); - ACT_SECONDS = _configuration.GetValue("AccessQueue:ActivitySeconds"); - CAPACITY_LIMIT = _configuration.GetValue("AccessQueue:CapacityLimit"); - ROLLING_EXPIRATION = _configuration.GetValue("AccessQueue:RollingExpiration"); + _config = new AccessQueueConfig + { + ExpirationSeconds = _configuration.GetValue("AccessQueue:ExpirationSeconds"), + ActivitySeconds = _configuration.GetValue("AccessQueue:ActivitySeconds"), + CapacityLimit = _configuration.GetValue("AccessQueue:CapacityLimit"), + RollingExpiration = _configuration.GetValue("AccessQueue:RollingExpiration") + }; + } + public AccessQueueConfig GetConfiguration() => _config.Clone(); + public void UpdateConfiguration(AccessQueueConfig config) + { + _config = config.Clone(); + } + public void PatchConfiguration(AccessQueueConfig partialConfig) + { + if (partialConfig.CapacityLimit.HasValue) _config.CapacityLimit = partialConfig.CapacityLimit.Value; + if (partialConfig.ActivitySeconds.HasValue) _config.ActivitySeconds = partialConfig.ActivitySeconds.Value; + if (partialConfig.ExpirationSeconds.HasValue) _config.ExpirationSeconds = partialConfig.ExpirationSeconds.Value; + if (partialConfig.RollingExpiration.HasValue) _config.RollingExpiration = partialConfig.RollingExpiration.Value; } public int UnexpiredTicketsCount => _accessQueueRepo.GetUnexpiredTicketsCount(); - public int ActiveTicketsCount => _accessQueueRepo.GetActiveTicketsCount(DateTime.UtcNow.AddSeconds(-_configuration.GetValue("AccessQueue:ActivitySeconds"))); + public int ActiveTicketsCount => _accessQueueRepo.GetActiveTicketsCount(DateTime.UtcNow.AddSeconds(-_config.ActivitySeconds.Value)); public int QueueCount => _accessQueueRepo.GetQueueCount(); public async Task RequestAccess(string userId) { await _queueLock.WaitAsync(); try { - var hasCapacity = !_accessQueueRepo.DidDequeueUntilFull(ACT_SECONDS, EXP_SECONDS, CAPACITY_LIMIT); + var hasCapacity = !_accessQueueRepo.DidDequeueUntilFull( + _config.ActivitySeconds.Value, + _config.ExpirationSeconds.Value, + _config.CapacityLimit.Value); var existingTicket = _accessQueueRepo.GetTicket(userId); if (existingTicket != null && existingTicket.ExpiresOn > DateTime.UtcNow) { // Already has access var expiresOn = existingTicket.ExpiresOn; - if (ROLLING_EXPIRATION) + if (_config.RollingExpiration.Value) { - expiresOn = DateTime.UtcNow.AddSeconds(EXP_SECONDS); + expiresOn = DateTime.UtcNow.AddSeconds(_config.ExpirationSeconds.Value); } _accessQueueRepo.UpsertTicket(new AccessTicket { @@ -62,7 +77,7 @@ namespace AccessQueueService.Services var accessTicket = new AccessTicket { UserId = userId, - ExpiresOn = DateTime.UtcNow.AddSeconds(EXP_SECONDS), + ExpiresOn = DateTime.UtcNow.AddSeconds(_config.ExpirationSeconds.Value), LastActive = DateTime.UtcNow }; _accessQueueRepo.UpsertTicket(accessTicket); diff --git a/AccessQueueService/Services/IAccessService.cs b/AccessQueueService/Services/IAccessService.cs index 6b94042..35cdf3c 100644 --- a/AccessQueueService/Services/IAccessService.cs +++ b/AccessQueueService/Services/IAccessService.cs @@ -7,5 +7,8 @@ namespace AccessQueueService.Services public Task RequestAccess(string userId); public Task RevokeAccess(string userId); public Task DeleteExpiredTickets(); + public AccessQueueConfig GetConfiguration(); + public void UpdateConfiguration(AccessQueueConfig config); + public void PatchConfiguration(AccessQueueConfig partialConfig); } } -- 2.40.1 From aa8e4942119faaa492b3b6b9d8b9fd98691126f6 Mon Sep 17 00:00:00 2001 From: Henry Hobbs Date: Sat, 17 May 2025 22:27:42 -0400 Subject: [PATCH 2/3] Initial pass on adding config page --- .../Components/Layout/MainLayout.razor | 3 + .../Components/Pages/Config.razor | 104 ++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 AccessQueuePlayground/Components/Pages/Config.razor diff --git a/AccessQueuePlayground/Components/Layout/MainLayout.razor b/AccessQueuePlayground/Components/Layout/MainLayout.razor index e21aaad..ee658cd 100644 --- a/AccessQueuePlayground/Components/Layout/MainLayout.razor +++ b/AccessQueuePlayground/Components/Layout/MainLayout.razor @@ -14,6 +14,9 @@

+ diff --git a/AccessQueuePlayground/Components/Pages/Config.razor b/AccessQueuePlayground/Components/Pages/Config.razor new file mode 100644 index 0000000..19bd521 --- /dev/null +++ b/AccessQueuePlayground/Components/Pages/Config.razor @@ -0,0 +1,104 @@ +@page "/config" +@inject AccessQueuePlayground.Services.IAccessQueueManager QueueManager +@using BlazorBootstrap + +

Access Queue Configuration

+ + + + +
+ + + @if (!isCapacityLimitValid) + { +
Please enter a positive integer.
+ } +
+
+ + + @if (!isActivitySecondsValid) + { +
Please enter a positive integer.
+ } +
+
+ + + @if (!isExpirationSecondsValid) + { +
Please enter a positive integer.
+ } +
+
+ +
+ + @if (successMessage != null) + { + @successMessage + } +
+ +@code { + private AccessQueueService.Models.AccessQueueConfig config = new(); + private string? capacityLimitInput; + private string? activitySecondsInput; + private string? expirationSecondsInput; + private bool rollingExpirationSwitch; + private bool isCapacityLimitValid = true; + private bool isActivitySecondsValid = true; + private bool isExpirationSecondsValid = true; + private string? successMessage; + + protected override void OnInitialized() + { + var current = QueueManager.GetConfig(); + config = current.Clone(); + capacityLimitInput = config.CapacityLimit?.ToString(); + activitySecondsInput = config.ActivitySeconds?.ToString(); + expirationSecondsInput = config.ExpirationSeconds?.ToString(); + rollingExpirationSwitch = config.RollingExpiration ?? true; + ValidateInputs(); + } + + private bool IsFormValid => isCapacityLimitValid && isActivitySecondsValid && isExpirationSecondsValid; + + private void ValidateInputs() + { + isCapacityLimitValid = int.TryParse(capacityLimitInput, out var cap) && cap > 0; + isActivitySecondsValid = int.TryParse(activitySecondsInput, out var act) && act > 0; + isExpirationSecondsValid = int.TryParse(expirationSecondsInput, out var exp) && exp > 0; + } + + private async Task HandleValidSubmit() + { + ValidateInputs(); + if (!IsFormValid) + return; + config.CapacityLimit = int.Parse(capacityLimitInput!); + config.ActivitySeconds = int.Parse(activitySecondsInput!); + config.ExpirationSeconds = int.Parse(expirationSecondsInput!); + config.RollingExpiration = rollingExpirationSwitch; + await Task.Run(() => QueueManager.UpdateConfig(config)); + successMessage = "Configuration updated successfully."; + } + + private void OnInputChanged(ChangeEventArgs e, string field) + { + switch (field) + { + case nameof(config.CapacityLimit): + capacityLimitInput = e.Value?.ToString(); + break; + case nameof(config.ActivitySeconds): + activitySecondsInput = e.Value?.ToString(); + break; + case nameof(config.ExpirationSeconds): + expirationSecondsInput = e.Value?.ToString(); + break; + } + ValidateInputs(); + } +} -- 2.40.1 From 7cb1f0a34c3497b970ac9c2b007b28f43aad0a76 Mon Sep 17 00:00:00 2001 From: henry Date: Sun, 18 May 2025 03:18:11 -0400 Subject: [PATCH 3/3] Simplified config page and fixed runtime issues --- .../Components/Pages/Config.razor | 168 +++++++++--------- 1 file changed, 80 insertions(+), 88 deletions(-) diff --git a/AccessQueuePlayground/Components/Pages/Config.razor b/AccessQueuePlayground/Components/Pages/Config.razor index 19bd521..05288e2 100644 --- a/AccessQueuePlayground/Components/Pages/Config.razor +++ b/AccessQueuePlayground/Components/Pages/Config.razor @@ -5,100 +5,92 @@

Access Queue Configuration

- - -
- - - @if (!isCapacityLimitValid) - { -
Please enter a positive integer.
- } -
-
- - - @if (!isActivitySecondsValid) - { -
Please enter a positive integer.
- } -
-
- - - @if (!isExpirationSecondsValid) - { -
Please enter a positive integer.
- } -
-
- -
- - @if (successMessage != null) - { - @successMessage - } + + +
+ + + @if (!isCapacityLimitValid) + { +
Please enter a positive integer.
+ } +
+
+ + + @if (!isActivitySecondsValid) + { +
Please enter a positive integer.
+ } +
+
+ + + @if (!isExpirationSecondsValid) + { +
Please enter a positive integer.
+ } +
+
+ +
+ + @if (successMessage != null) + { + @successMessage + }
@code { - private AccessQueueService.Models.AccessQueueConfig config = new(); - private string? capacityLimitInput; - private string? activitySecondsInput; - private string? expirationSecondsInput; - private bool rollingExpirationSwitch; - private bool isCapacityLimitValid = true; - private bool isActivitySecondsValid = true; - private bool isExpirationSecondsValid = true; - private string? successMessage; + private ConfigModel config = new(); + private bool isCapacityLimitValid = true; + private bool isActivitySecondsValid = true; + private bool isExpirationSecondsValid = true; + private string? successMessage; - protected override void OnInitialized() - { - var current = QueueManager.GetConfig(); - config = current.Clone(); - capacityLimitInput = config.CapacityLimit?.ToString(); - activitySecondsInput = config.ActivitySeconds?.ToString(); - expirationSecondsInput = config.ExpirationSeconds?.ToString(); - rollingExpirationSwitch = config.RollingExpiration ?? true; - ValidateInputs(); - } + protected override void OnInitialized() + { + var current = QueueManager.GetConfig(); + config = new ConfigModel + { + ActivitySeconds = (current.ActivitySeconds ?? 0).ToString(), + CapacityLimit = (current.CapacityLimit ?? 0).ToString(), + ExpirationSeconds = (current.ExpirationSeconds ?? 0).ToString(), + RollingExpiration = current.RollingExpiration ?? false + }; + ValidateInputs(); + } - private bool IsFormValid => isCapacityLimitValid && isActivitySecondsValid && isExpirationSecondsValid; + private bool IsFormValid => isCapacityLimitValid && isActivitySecondsValid && isExpirationSecondsValid; - private void ValidateInputs() - { - isCapacityLimitValid = int.TryParse(capacityLimitInput, out var cap) && cap > 0; - isActivitySecondsValid = int.TryParse(activitySecondsInput, out var act) && act > 0; - isExpirationSecondsValid = int.TryParse(expirationSecondsInput, out var exp) && exp > 0; - } + private void ValidateInputs() + { + isCapacityLimitValid = int.TryParse(config.CapacityLimit, out var cap) && cap > 0; + isActivitySecondsValid = int.TryParse(config.ActivitySeconds, out var act) && act > 0; + isExpirationSecondsValid = int.TryParse(config.ExpirationSeconds, out var exp) && exp > 0; + } - private async Task HandleValidSubmit() - { - ValidateInputs(); - if (!IsFormValid) - return; - config.CapacityLimit = int.Parse(capacityLimitInput!); - config.ActivitySeconds = int.Parse(activitySecondsInput!); - config.ExpirationSeconds = int.Parse(expirationSecondsInput!); - config.RollingExpiration = rollingExpirationSwitch; - await Task.Run(() => QueueManager.UpdateConfig(config)); - successMessage = "Configuration updated successfully."; - } + private async Task HandleValidSubmit() + { + successMessage = null; + ValidateInputs(); + if (!IsFormValid) + return; + await Task.Run(() => QueueManager.UpdateConfig(new () + { + ActivitySeconds = int.Parse(config.ActivitySeconds), + CapacityLimit = int.Parse(config.CapacityLimit), + ExpirationSeconds = int.Parse(config.ExpirationSeconds), + RollingExpiration = config.RollingExpiration + })); + successMessage = "Configuration updated successfully."; + } - private void OnInputChanged(ChangeEventArgs e, string field) - { - switch (field) - { - case nameof(config.CapacityLimit): - capacityLimitInput = e.Value?.ToString(); - break; - case nameof(config.ActivitySeconds): - activitySecondsInput = e.Value?.ToString(); - break; - case nameof(config.ExpirationSeconds): - expirationSecondsInput = e.Value?.ToString(); - break; - } - ValidateInputs(); - } + public class ConfigModel + { + public string CapacityLimit { get; set; } = ""; + public string ActivitySeconds { get; set; } = ""; + public string ExpirationSeconds { get; set; } = ""; + public bool RollingExpiration { get; set; } + } } -- 2.40.1