From eca61933bfe2b72e1a6e034dd676e643ea077c06 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Sun, 1 Sep 2024 17:11:52 +0200 Subject: [PATCH] Add log truncate buttons to admin (#180) --- .../Main/Layout/MainLayout.razor | 2 + src/AliasVault.Admin/Main/Pages/Emails.razor | 2 +- .../Main/Pages/Logging/Auth.razor | 26 +++- .../Main/Pages/Logging/General.razor | 26 +++- src/AliasVault.Admin/Main/Pages/MainBase.cs | 7 ++ .../Main/Pages/Users/Users.razor | 2 +- src/AliasVault.Admin/Main/_Imports.razor | 1 + src/AliasVault.Admin/Program.cs | 2 + src/AliasVault.Admin/wwwroot/css/tailwind.css | 113 ++++++++++++++++++ .../Main/Layout/MainLayout.razor | 1 + .../Main/Pages/Credentials/Home.razor | 2 +- .../Main/Pages/Emails/Home.razor | 2 +- .../Pages/Settings/Security/Security.razor | 2 +- src/AliasVault.Client/Program.cs | 2 + src/AliasVault.Client/_Imports.razor | 1 + .../wwwroot/css/tailwind.css | 48 ++++++++ .../Buttons/Button.razor | 67 +++++++++++ .../Buttons/DeleteButton.razor | 29 +++++ .../Buttons/RefreshButton.razor | 76 ++++++++++++ .../ConfirmModal.razor | 39 ++++++ .../RefreshButton.razor | 52 -------- .../Services/ConfirmModalService.cs | 68 +++++++++++ 22 files changed, 511 insertions(+), 59 deletions(-) create mode 100644 src/Utilities/AliasVault.RazorComponents/Buttons/Button.razor create mode 100644 src/Utilities/AliasVault.RazorComponents/Buttons/DeleteButton.razor create mode 100644 src/Utilities/AliasVault.RazorComponents/Buttons/RefreshButton.razor create mode 100644 src/Utilities/AliasVault.RazorComponents/ConfirmModal.razor delete mode 100644 src/Utilities/AliasVault.RazorComponents/RefreshButton.razor create mode 100644 src/Utilities/AliasVault.RazorComponents/Services/ConfirmModalService.cs diff --git a/src/AliasVault.Admin/Main/Layout/MainLayout.razor b/src/AliasVault.Admin/Main/Layout/MainLayout.razor index 428d42623..bbbd02729 100644 --- a/src/AliasVault.Admin/Main/Layout/MainLayout.razor +++ b/src/AliasVault.Admin/Main/Layout/MainLayout.razor @@ -4,6 +4,7 @@ @inject GlobalLoadingService GlobalLoadingService +
@@ -15,6 +16,7 @@
+
An unhandled error has occurred. Reload diff --git a/src/AliasVault.Admin/Main/Pages/Emails.razor b/src/AliasVault.Admin/Main/Pages/Emails.razor index f224d9e1c..be6c33298 100644 --- a/src/AliasVault.Admin/Main/Pages/Emails.razor +++ b/src/AliasVault.Admin/Main/Pages/Emails.razor @@ -8,7 +8,7 @@

Emails

- +

This page gives an overview of recently received mails by this AliasVault server.

diff --git a/src/AliasVault.Admin/Main/Pages/Logging/Auth.razor b/src/AliasVault.Admin/Main/Pages/Logging/Auth.razor index 9dff19894..ebb65e08e 100644 --- a/src/AliasVault.Admin/Main/Pages/Logging/Auth.razor +++ b/src/AliasVault.Admin/Main/Pages/Logging/Auth.razor @@ -9,7 +9,10 @@

Auth logs

- +
+ + +

This page gives an overview of recent auth attempts.

@@ -146,4 +149,25 @@ else IsLoading = false; StateHasChanged(); } + + private async Task DeleteLogsWithConfirmation() + { + if (await ConfirmModalService.ShowConfirmation("Confirm Delete", "Are you sure you want to delete all logs? This action cannot be undone.")) + { + await DeleteLogs(); + } + } + + private async Task DeleteLogs() + { + IsLoading = true; + StateHasChanged(); + + DbContext.AuthLogs.RemoveRange(DbContext.AuthLogs); + await DbContext.SaveChangesAsync(); + await RefreshData(); + + IsLoading = false; + StateHasChanged(); + } } diff --git a/src/AliasVault.Admin/Main/Pages/Logging/General.razor b/src/AliasVault.Admin/Main/Pages/Logging/General.razor index 07d9ad972..da2a57af8 100644 --- a/src/AliasVault.Admin/Main/Pages/Logging/General.razor +++ b/src/AliasVault.Admin/Main/Pages/Logging/General.razor @@ -8,7 +8,10 @@

General logs

- +
+ + +

This page gives an overview of recent system logs.

@@ -165,4 +168,25 @@ else IsLoading = false; StateHasChanged(); } + + private async Task DeleteLogsWithConfirmation() + { + if (await ConfirmModalService.ShowConfirmation("Confirm Delete", "Are you sure you want to delete all logs? This action cannot be undone.")) + { + await DeleteLogs(); + } + } + + private async Task DeleteLogs() + { + IsLoading = true; + StateHasChanged(); + + DbContext.Logs.RemoveRange(DbContext.Logs); + await DbContext.SaveChangesAsync(); + await RefreshData(); + + IsLoading = false; + StateHasChanged(); + } } diff --git a/src/AliasVault.Admin/Main/Pages/MainBase.cs b/src/AliasVault.Admin/Main/Pages/MainBase.cs index c8ff6270a..adb989b95 100644 --- a/src/AliasVault.Admin/Main/Pages/MainBase.cs +++ b/src/AliasVault.Admin/Main/Pages/MainBase.cs @@ -11,6 +11,7 @@ using AliasServerDb; using AliasVault.Admin.Main.Models; using AliasVault.Admin.Services; using AliasVault.AuthLogging; +using AliasVault.RazorComponents.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Components; using Microsoft.EntityFrameworkCore; @@ -72,6 +73,12 @@ public class MainBase : OwningComponentBase [Inject] protected AuthLoggingService AuthLoggingService { get; set; } = null!; + /// + /// Gets or sets the confirm modal service. + /// + [Inject] + protected ConfirmModalService ConfirmModalService { get; set; } = null!; + /// /// Gets or sets the injected JSRuntime instance. /// diff --git a/src/AliasVault.Admin/Main/Pages/Users/Users.razor b/src/AliasVault.Admin/Main/Pages/Users/Users.razor index 88536ae4b..c4a134d93 100644 --- a/src/AliasVault.Admin/Main/Pages/Users/Users.razor +++ b/src/AliasVault.Admin/Main/Pages/Users/Users.razor @@ -8,7 +8,7 @@

Users

- +

This page gives an overview of all registered users and the associated vaults.

diff --git a/src/AliasVault.Admin/Main/_Imports.razor b/src/AliasVault.Admin/Main/_Imports.razor index d42fe83af..50d9a4ba8 100644 --- a/src/AliasVault.Admin/Main/_Imports.razor +++ b/src/AliasVault.Admin/Main/_Imports.razor @@ -19,6 +19,7 @@ @using AliasVault.Admin.Main.Components.WorkerStatus @using AliasVault.RazorComponents @using AliasVault.RazorComponents.Alerts +@using AliasVault.RazorComponents.Buttons @using AliasVault.Admin.Main.Models @using AliasVault.Admin.Main.Pages @using AliasVault.Admin.Services diff --git a/src/AliasVault.Admin/Program.cs b/src/AliasVault.Admin/Program.cs index 793b3eb4e..b792c0774 100644 --- a/src/AliasVault.Admin/Program.cs +++ b/src/AliasVault.Admin/Program.cs @@ -15,6 +15,7 @@ using AliasVault.Admin.Main; using AliasVault.Admin.Services; using AliasVault.AuthLogging; using AliasVault.Logging; +using AliasVault.RazorComponents.Services; using Cryptography.Server; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Identity; @@ -48,6 +49,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddHttpContextAccessor(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddSingleton(new VersionedContentService(Directory.GetCurrentDirectory() + "/wwwroot")); builder.Services.AddAuthentication(options => diff --git a/src/AliasVault.Admin/wwwroot/css/tailwind.css b/src/AliasVault.Admin/wwwroot/css/tailwind.css index dc3848890..9b065e5cf 100644 --- a/src/AliasVault.Admin/wwwroot/css/tailwind.css +++ b/src/AliasVault.Admin/wwwroot/css/tailwind.css @@ -600,6 +600,10 @@ video { border-width: 0; } +.visible { + visibility: visible; +} + .invisible { visibility: hidden; } @@ -620,6 +624,10 @@ video { position: relative; } +.inset-0 { + inset: 0px; +} + .right-0 { right: 0px; } @@ -739,6 +747,14 @@ video { margin-top: 2rem; } +.mt-2 { + margin-top: 0.5rem; +} + +.mt-3 { + margin-top: 0.75rem; +} + .line-clamp-1 { overflow: hidden; display: -webkit-box; @@ -858,6 +874,10 @@ video { width: 100%; } +.w-96 { + width: 24rem; +} + .max-w-2xl { max-width: 42rem; } @@ -892,6 +912,16 @@ video { animation: spin 1s linear infinite; } +@keyframes pulse { + 50% { + opacity: .5; + } +} + +.animate-pulse { + animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; +} + .cursor-not-allowed { cursor: not-allowed; } @@ -936,6 +966,10 @@ video { align-items: flex-start; } +.items-end { + align-items: flex-end; +} + .items-center { align-items: center; } @@ -1004,6 +1038,12 @@ video { margin-bottom: calc(1.5rem * var(--tw-space-y-reverse)); } +.space-x-3 > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 0; + margin-right: calc(0.75rem * var(--tw-space-x-reverse)); + margin-left: calc(0.75rem * calc(1 - var(--tw-space-x-reverse))); +} + .divide-y > :not([hidden]) ~ :not([hidden]) { --tw-divide-y-reverse: 0; border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse))); @@ -1260,6 +1300,21 @@ video { background-color: rgb(234 179 8 / var(--tw-bg-opacity)); } +.bg-gray-300 { + --tw-bg-opacity: 1; + background-color: rgb(209 213 219 / var(--tw-bg-opacity)); +} + +.bg-gray-700 { + --tw-bg-opacity: 1; + background-color: rgb(55 65 81 / var(--tw-bg-opacity)); +} + +.bg-primary-500 { + --tw-bg-opacity: 1; + background-color: rgb(244 149 65 / var(--tw-bg-opacity)); +} + .bg-opacity-50 { --tw-bg-opacity: 0.5; } @@ -1284,6 +1339,10 @@ video { padding: 1.5rem; } +.p-5 { + padding: 1.25rem; +} + .px-2 { padding-left: 0.5rem; padding-right: 0.5rem; @@ -1349,6 +1408,11 @@ video { padding-bottom: 2rem; } +.px-7 { + padding-left: 1.75rem; + padding-right: 1.75rem; +} + .pl-2 { padding-left: 0.5rem; } @@ -1640,6 +1704,25 @@ video { background-color: rgb(153 27 27 / var(--tw-bg-opacity)); } +.hover\:bg-blue-700:hover { + --tw-bg-opacity: 1; + background-color: rgb(29 78 216 / var(--tw-bg-opacity)); +} + +.hover\:bg-gray-400:hover { + --tw-bg-opacity: 1; + background-color: rgb(156 163 175 / var(--tw-bg-opacity)); +} + +.hover\:bg-gray-800:hover { + --tw-bg-opacity: 1; + background-color: rgb(31 41 55 / var(--tw-bg-opacity)); +} + +.hover\:bg-opacity-80:hover { + --tw-bg-opacity: 0.8; +} + .hover\:text-gray-900:hover { --tw-text-opacity: 1; color: rgb(17 24 39 / var(--tw-text-opacity)); @@ -1731,6 +1814,16 @@ video { --tw-ring-color: rgb(252 165 165 / var(--tw-ring-opacity)); } +.focus\:ring-green-300:focus { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(134 239 172 / var(--tw-ring-opacity)); +} + +.focus\:ring-yellow-300:focus { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(253 224 71 / var(--tw-ring-opacity)); +} + .focus\:ring-offset-2:focus { --tw-ring-offset-width: 2px; } @@ -2013,6 +2106,26 @@ video { --tw-ring-color: rgb(127 29 29 / var(--tw-ring-opacity)); } +.dark\:focus\:ring-blue-800:focus:is(.dark *) { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(30 64 175 / var(--tw-ring-opacity)); +} + +.dark\:focus\:ring-green-800:focus:is(.dark *) { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(22 101 52 / var(--tw-ring-opacity)); +} + +.dark\:focus\:ring-yellow-800:focus:is(.dark *) { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(133 77 14 / var(--tw-ring-opacity)); +} + +.dark\:focus\:ring-gray-800:focus:is(.dark *) { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(31 41 55 / var(--tw-ring-opacity)); +} + @media (min-width: 640px) { .sm\:flex { display: flex; diff --git a/src/AliasVault.Client/Main/Layout/MainLayout.razor b/src/AliasVault.Client/Main/Layout/MainLayout.razor index 52ea04b2e..1350f1d11 100644 --- a/src/AliasVault.Client/Main/Layout/MainLayout.razor +++ b/src/AliasVault.Client/Main/Layout/MainLayout.razor @@ -5,6 +5,7 @@ +
diff --git a/src/AliasVault.Client/Main/Pages/Credentials/Home.razor b/src/AliasVault.Client/Main/Pages/Credentials/Home.razor index 0d39f7bab..757a28f78 100644 --- a/src/AliasVault.Client/Main/Pages/Credentials/Home.razor +++ b/src/AliasVault.Client/Main/Pages/Credentials/Home.razor @@ -9,7 +9,7 @@

Credentials

- +

Find all of your credentials below.

diff --git a/src/AliasVault.Client/Main/Pages/Emails/Home.razor b/src/AliasVault.Client/Main/Pages/Emails/Home.razor index 13108fc3f..4bbfb0b98 100644 --- a/src/AliasVault.Client/Main/Pages/Emails/Home.razor +++ b/src/AliasVault.Client/Main/Pages/Emails/Home.razor @@ -21,7 +21,7 @@

Emails

- +

Below you can find all recent emails sent to one of the email addresses used in your credentials.

diff --git a/src/AliasVault.Client/Main/Pages/Settings/Security/Security.razor b/src/AliasVault.Client/Main/Pages/Settings/Security/Security.razor index e802ea587..67c53f209 100644 --- a/src/AliasVault.Client/Main/Pages/Settings/Security/Security.razor +++ b/src/AliasVault.Client/Main/Pages/Settings/Security/Security.razor @@ -9,7 +9,7 @@

Security settings

- +

Configure security settings.

diff --git a/src/AliasVault.Client/Program.cs b/src/AliasVault.Client/Program.cs index d981a0f41..222a3ec27 100644 --- a/src/AliasVault.Client/Program.cs +++ b/src/AliasVault.Client/Program.cs @@ -7,6 +7,7 @@ using AliasVault.Client; using AliasVault.Client.Providers; +using AliasVault.RazorComponents.Services; using Blazored.LocalStorage; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.Web; @@ -72,6 +73,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddSingleton(); +builder.Services.AddScoped(); builder.Services.AddAuthorizationCore(); builder.Services.AddBlazoredLocalStorage(); diff --git a/src/AliasVault.Client/_Imports.razor b/src/AliasVault.Client/_Imports.razor index 53f730e7e..6793fed06 100644 --- a/src/AliasVault.Client/_Imports.razor +++ b/src/AliasVault.Client/_Imports.razor @@ -26,6 +26,7 @@ @using AliasVault.Client.Services.Database @using AliasVault.RazorComponents @using AliasVault.RazorComponents.Alerts +@using AliasVault.RazorComponents.Buttons @using Microsoft.AspNetCore.Components.Authorization @using Microsoft.AspNetCore.Components.WebAssembly.Authentication @using Blazored.LocalStorage diff --git a/src/AliasVault.Client/wwwroot/css/tailwind.css b/src/AliasVault.Client/wwwroot/css/tailwind.css index 736e6ff22..2eb131e14 100644 --- a/src/AliasVault.Client/wwwroot/css/tailwind.css +++ b/src/AliasVault.Client/wwwroot/css/tailwind.css @@ -600,6 +600,10 @@ video { border-width: 0; } +.visible { + visibility: visible; +} + .static { position: static; } @@ -1394,6 +1398,21 @@ video { background-color: rgb(255 255 255 / var(--tw-bg-opacity)); } +.bg-gray-300 { + --tw-bg-opacity: 1; + background-color: rgb(209 213 219 / var(--tw-bg-opacity)); +} + +.bg-yellow-500 { + --tw-bg-opacity: 1; + background-color: rgb(234 179 8 / var(--tw-bg-opacity)); +} + +.bg-red-700 { + --tw-bg-opacity: 1; + background-color: rgb(185 28 28 / var(--tw-bg-opacity)); +} + .bg-opacity-50 { --tw-bg-opacity: 0.5; } @@ -1921,6 +1940,15 @@ video { background-color: rgb(153 27 27 / var(--tw-bg-opacity)); } +.hover\:bg-gray-400:hover { + --tw-bg-opacity: 1; + background-color: rgb(156 163 175 / var(--tw-bg-opacity)); +} + +.hover\:bg-opacity-80:hover { + --tw-bg-opacity: 0.8; +} + .hover\:from-primary-600:hover { --tw-gradient-from: #d68338 var(--tw-gradient-from-position); --tw-gradient-to: rgb(214 131 56 / 0) var(--tw-gradient-to-position); @@ -2062,6 +2090,11 @@ video { --tw-ring-color: rgb(239 68 68 / var(--tw-ring-opacity)); } +.focus\:ring-yellow-300:focus { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(253 224 71 / var(--tw-ring-opacity)); +} + .focus\:ring-offset-2:focus { --tw-ring-offset-width: 2px; } @@ -2147,6 +2180,11 @@ video { background-color: rgb(30 41 59 / var(--tw-bg-opacity)); } +.dark\:bg-red-600:is(.dark *) { + --tw-bg-opacity: 1; + background-color: rgb(220 38 38 / var(--tw-bg-opacity)); +} + .dark\:bg-opacity-80:is(.dark *) { --tw-bg-opacity: 0.8; } @@ -2280,6 +2318,11 @@ video { background-color: rgb(220 38 38 / var(--tw-bg-opacity)); } +.dark\:hover\:bg-red-700:hover:is(.dark *) { + --tw-bg-opacity: 1; + background-color: rgb(185 28 28 / var(--tw-bg-opacity)); +} + .dark\:hover\:from-primary-500:hover:is(.dark *) { --tw-gradient-from: #f49541 var(--tw-gradient-from-position); --tw-gradient-to: rgb(244 149 65 / 0) var(--tw-gradient-to-position); @@ -2375,6 +2418,11 @@ video { --tw-ring-color: rgb(127 29 29 / var(--tw-ring-opacity)); } +.dark\:focus\:ring-yellow-800:focus:is(.dark *) { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(133 77 14 / var(--tw-ring-opacity)); +} + @media (min-width: 640px) { .sm\:col-span-3 { grid-column: span 3 / span 3; diff --git a/src/Utilities/AliasVault.RazorComponents/Buttons/Button.razor b/src/Utilities/AliasVault.RazorComponents/Buttons/Button.razor new file mode 100644 index 000000000..cc1642f3d --- /dev/null +++ b/src/Utilities/AliasVault.RazorComponents/Buttons/Button.razor @@ -0,0 +1,67 @@ + + +@code { + /// + /// The content to be displayed inside the button. + /// + [Parameter] + public RenderFragment? ChildContent { get; set; } + + /// + /// The event to call when the button is clicked. + /// + [Parameter] + public EventCallback OnClick { get; set; } + + /// + /// Specifies whether the button is disabled. + /// + [Parameter] + public bool IsDisabled { get; set; } + + /// + /// The color theme of the button. + /// + [Parameter] + public string Color { get; set; } = "primary"; + + /// + /// Additional CSS classes to apply to the button. + /// + [Parameter] + public string AdditionalClasses { get; set; } = ""; + + /// + /// Handles the button click event. + /// + private async Task HandleClick() + { + if (!IsDisabled) + { + await OnClick.InvokeAsync(); + } + } + + /// + /// Gets the CSS classes for the button based on its state and color. + /// + /// A string containing the CSS classes for the button. + private string GetButtonClasses() + { + var baseClasses = "flex center items-center px-3 py-2 text-sm font-medium text-white rounded-lg focus:outline-none focus:ring-4"; + var colorClasses = Color switch + { + "primary" => "bg-primary-700 hover:bg-primary-800 focus:ring-primary-300 dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800", + "danger" => "bg-red-700 hover:bg-red-800 focus:ring-red-300 dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-800", + "secondary" => "bg-secondary-700 hover:bg-secondary-800 focus:ring-secondary-300 dark:bg-secondary-600 dark:hover:bg-secondary-700 dark:focus:ring-secondary-800", + _ => "bg-gray-700 hover:bg-gray-800 focus:ring-gray-300 dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-800" + }; + var disabledClasses = IsDisabled ? "bg-gray-400 cursor-not-allowed" : ""; + + return $"{baseClasses} {colorClasses} {disabledClasses} {AdditionalClasses}".Trim(); + } +} diff --git a/src/Utilities/AliasVault.RazorComponents/Buttons/DeleteButton.razor b/src/Utilities/AliasVault.RazorComponents/Buttons/DeleteButton.razor new file mode 100644 index 000000000..0452707e2 --- /dev/null +++ b/src/Utilities/AliasVault.RazorComponents/Buttons/DeleteButton.razor @@ -0,0 +1,29 @@ + + +@code { + /// + /// The event to call in the parent when the delete button is clicked. + /// + [Parameter] + public EventCallback OnClick { get; set; } + + /// + /// The text to display on the button. + /// + [Parameter] + public string ButtonText { get; set; } = "Delete"; + + /// + /// Handles the button click event. + /// + private async Task HandleClick() + { + await OnClick.InvokeAsync(); + } +} diff --git a/src/Utilities/AliasVault.RazorComponents/Buttons/RefreshButton.razor b/src/Utilities/AliasVault.RazorComponents/Buttons/RefreshButton.razor new file mode 100644 index 000000000..c2280ad82 --- /dev/null +++ b/src/Utilities/AliasVault.RazorComponents/Buttons/RefreshButton.razor @@ -0,0 +1,76 @@ +@using System.Timers + + + +@code { + /// + /// The event to call in the parent when the button is clicked. + /// + [Parameter] + public EventCallback OnClick { get; set; } + + /// + /// The text to display on the button. + /// + [Parameter] + public string ButtonText { get; set; } = "Refresh"; + + /// + /// The color theme of the button. + /// + [Parameter] + public string Color { get; set; } = "primary"; + + /// + /// Additional CSS classes to apply to the button. + /// + [Parameter] + public string AdditionalClasses { get; set; } = ""; + + /// + /// Indicates whether the button is currently in a refreshing state. + /// + private bool IsRefreshing; + + /// + /// Timer used to control the refreshing state duration. + /// + private Timer Timer = new(); + + /// + /// Handles the button click event. + /// + private async Task HandleClick() + { + if (IsRefreshing) return; + + IsRefreshing = true; + await OnClick.InvokeAsync(); + + Timer = new Timer(500); + Timer.Elapsed += (sender, args) => + { + IsRefreshing = false; + Timer.Dispose(); + InvokeAsync(StateHasChanged); + }; + Timer.Start(); + } + + /// + /// Gets the CSS classes for the refresh icon based on the refreshing state. + /// + /// A string containing the CSS classes for the icon. + private string GetIconClasses() + { + return $"w-4 h-4 {(IsRefreshing ? "animate-spin" : "")}"; + } +} diff --git a/src/Utilities/AliasVault.RazorComponents/ConfirmModal.razor b/src/Utilities/AliasVault.RazorComponents/ConfirmModal.razor new file mode 100644 index 000000000..8a8d57929 --- /dev/null +++ b/src/Utilities/AliasVault.RazorComponents/ConfirmModal.razor @@ -0,0 +1,39 @@ +@inject ConfirmModalService ModalService +@using AliasVault.RazorComponents.Services +@implements IDisposable + +@if (ModalService.IsVisible) +{ +
+
+
+

@ModalService.Title

+
+

+ @ModalService.Message +

+
+
+ + +
+
+
+
+} + +@code { + protected override void OnInitialized() + { + ModalService.OnChange += StateHasChanged; + } + + public void Dispose() + { + ModalService.OnChange -= StateHasChanged; + } +} diff --git a/src/Utilities/AliasVault.RazorComponents/RefreshButton.razor b/src/Utilities/AliasVault.RazorComponents/RefreshButton.razor deleted file mode 100644 index 9ce054a63..000000000 --- a/src/Utilities/AliasVault.RazorComponents/RefreshButton.razor +++ /dev/null @@ -1,52 +0,0 @@ -@using System.Timers - - - -@code { - /// - /// The event to call in the parent when the button is clicked. - /// - [Parameter] public EventCallback OnRefresh { get; set; } - - /// - /// The text to display on the button. - /// - [Parameter] public string ButtonText { get; set; } = "Refresh"; - - private bool IsRefreshing; - private Timer Timer = new(); - - private async Task HandleClick() - { - if (IsRefreshing) return; - - IsRefreshing = true; - await OnRefresh.InvokeAsync(); - - Timer = new Timer(500); - Timer.Elapsed += (sender, args) => - { - IsRefreshing = false; - Timer.Dispose(); - InvokeAsync(StateHasChanged); - }; - Timer.Start(); - } - - private string GetButtonClasses() - { - return $"flex items-center px-3 py-2 text-sm font-medium text-white rounded-lg focus:outline-none focus:ring-4 focus:ring-primary-300 dark:focus:ring-primary-800 {(IsRefreshing ? "bg-gray-400 cursor-not-allowed" : "bg-primary-700 hover:bg-primary-800 dark:bg-primary-600 dark:hover:bg-primary-700")}"; - } - - private string GetIconClasses() - { - return $"w-4 h-4 {(IsRefreshing ? "animate-spin" : "")}"; - } -} diff --git a/src/Utilities/AliasVault.RazorComponents/Services/ConfirmModalService.cs b/src/Utilities/AliasVault.RazorComponents/Services/ConfirmModalService.cs new file mode 100644 index 000000000..20c679ad4 --- /dev/null +++ b/src/Utilities/AliasVault.RazorComponents/Services/ConfirmModalService.cs @@ -0,0 +1,68 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- + +namespace AliasVault.RazorComponents.Services; + +using System; +using System.Threading.Tasks; + +/// +/// Service for managing confirmation modals. +/// +public class ConfirmModalService +{ + /// + /// Event triggered when the modal state changes. + /// + public event Action OnChange = default!; + + /// + /// Gets or sets the title of the modal. + /// + public string Title { get; private set; } = "Are you sure?"; + + /// + /// Gets or sets the message of the modal. + /// + public string Message { get; private set; } = "Are you sure you want to do this?"; + + /// + /// Gets or sets a value indicating whether the modal is visible. + /// + public bool IsVisible { get; private set; } + + private TaskCompletionSource _modalTaskCompletionSource = default!; + + /// + /// Shows the confirmation modal and waits for user response. + /// + /// The title of the modal. + /// The message to display in the modal. + /// A task that completes when the user responds, returning true if confirmed, false if cancelled. + public Task ShowConfirmation(string title, string message) + { + Title = title; + Message = message; + IsVisible = true; + _modalTaskCompletionSource = new TaskCompletionSource(); + NotifyStateChanged(); + return _modalTaskCompletionSource.Task; + } + + /// + /// Closes the modal with the specified result. + /// + /// The result of the confirmation. + public void CloseModal(bool result) + { + IsVisible = false; + _modalTaskCompletionSource.TrySetResult(result); + NotifyStateChanged(); + } + + private void NotifyStateChanged() => OnChange?.Invoke(); +}