From 7b53e7b6e5f33b035435c03c5954b9c61cbc37f1 Mon Sep 17 00:00:00 2001 From: nickle Date: Sun, 28 Jan 2024 14:20:41 -0500 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=90=20Adding=20copy=20subscriptions=20?= =?UTF-8?q?and=20user=20deletion=20to=20UI=20=F0=9F=A9=B9=20Fixing=20issue?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Users/IO/UserVerification.cs | 12 +- .../Users/Types/UserInformation.cs | 2 +- .../Users/OnCopySubscriptions.cs | 8 +- .../Users/OnSubscriptionsCleanup.cs | 5 +- .../Features/Admin/Admin.razor | 6 + .../Features/Admin/AdminSeriesCard.razor | 9 +- .../Features/Admin/Endpoints.cs | 24 +++ .../Features/Admin/RenderFragments.cs | 44 ++++ .../Features/Admin/Types.cs | 14 ++ .../Features/Admin/UserNotFoundResult.razor | 18 ++ .../Features/Admin/Users.razor | 65 ++++++ .../Common/DefaultResponses/Extensions.cs | 4 +- src/AnimeFeedManager.Web/wwwroot/app.css | 192 ++++-------------- 13 files changed, 233 insertions(+), 170 deletions(-) create mode 100644 src/AnimeFeedManager.Web/Features/Admin/RenderFragments.cs create mode 100644 src/AnimeFeedManager.Web/Features/Admin/Types.cs create mode 100644 src/AnimeFeedManager.Web/Features/Admin/UserNotFoundResult.razor create mode 100644 src/AnimeFeedManager.Web/Features/Admin/Users.razor diff --git a/src/AnimeFeedManager.Features/Users/IO/UserVerification.cs b/src/AnimeFeedManager.Features/Users/IO/UserVerification.cs index 0a62fa2b..d184a891 100644 --- a/src/AnimeFeedManager.Features/Users/IO/UserVerification.cs +++ b/src/AnimeFeedManager.Features/Users/IO/UserVerification.cs @@ -36,16 +36,22 @@ public Task> CheckUsersExist(CancellationToken t .BindAsync(client => TableUtils.ExecuteQueryWithNotFound(() => client.QueryAsync( - u => u.PartitionKey == Constants.UserPartitionKey && usersString.Contains(u.RowKey), + GetUserFilter(usersString), cancellationToken: token))) .MapAsync(matches => ExtractResults(matches, usersString)); } - private UsersCheck ExtractResults(ImmutableList matches, IEnumerable targets) + private static UsersCheck ExtractResults(ImmutableList matches, IEnumerable targets) { if (matches.Count == targets.Count()) return new AllMatched(); // At this point we guarantee there is at least a match - return new SomeNotFound(targets.Except(matches.Select(m => m.PartitionKey)).ToImmutableList()); + return new SomeNotFound(targets.Except(matches.Select(m => m.RowKey)).ToImmutableList()); + } + + private static string GetUserFilter(IEnumerable users) + { + return users.Aggregate(TableClient.CreateQueryFilter($"PartitionKey eq {Constants.UserPartitionKey}"), + (current, user) => current + TableClient.CreateQueryFilter($" or RowKey eq {user}")); } } \ No newline at end of file diff --git a/src/AnimeFeedManager.Features/Users/Types/UserInformation.cs b/src/AnimeFeedManager.Features/Users/Types/UserInformation.cs index 40a8c566..f1a05a52 100644 --- a/src/AnimeFeedManager.Features/Users/Types/UserInformation.cs +++ b/src/AnimeFeedManager.Features/Users/Types/UserInformation.cs @@ -6,4 +6,4 @@ public abstract record UsersCheck; public record AllMatched : UsersCheck; -public record SomeNotFound(ImmutableList notFoundUsers): UsersCheck; \ No newline at end of file +public record SomeNotFound(ImmutableList NotFoundUsers): UsersCheck; \ No newline at end of file diff --git a/src/AnimeFeedManager.Functions/Users/OnCopySubscriptions.cs b/src/AnimeFeedManager.Functions/Users/OnCopySubscriptions.cs index b967bd6b..620aae8c 100644 --- a/src/AnimeFeedManager.Functions/Users/OnCopySubscriptions.cs +++ b/src/AnimeFeedManager.Functions/Users/OnCopySubscriptions.cs @@ -24,8 +24,8 @@ public async Task Run( [QueueTrigger(Box.Available.SubscriptionsCopyBox, Connection = "AzureWebJobsStorage")] CopySubscriptionRequest notification) { - - _logger.LogInformation("Trying to copy subscriptions from {Source} to {Target}", notification.sourceId, notification.targetId); + _logger.LogInformation("Trying to copy subscriptions from {Source} to {Target}", notification.sourceId, + notification.targetId); var result = await (UserIdValidator.Validate(notification.sourceId), UserIdValidator.Validate(notification.targetId)) .Apply((source, target) => (source, target)) @@ -40,8 +40,8 @@ private void LogResults(ImmutableList results) { foreach (var result in results) { - _logger.LogError("[{Type}]: {Count} entries have been copied", result.Completed.ToString(), - result.Scope.ToString()); + _logger.LogInformation("[{Type}]: {Count} entries have been copied", result.Scope.ToString(), + result.Completed.ToString()); } } } \ No newline at end of file diff --git a/src/AnimeFeedManager.Functions/Users/OnSubscriptionsCleanup.cs b/src/AnimeFeedManager.Functions/Users/OnSubscriptionsCleanup.cs index d5bd757e..68adb5f2 100644 --- a/src/AnimeFeedManager.Functions/Users/OnSubscriptionsCleanup.cs +++ b/src/AnimeFeedManager.Functions/Users/OnSubscriptionsCleanup.cs @@ -36,8 +36,9 @@ private void LogResults(ImmutableList results) { foreach (var result in results) { - _logger.LogError("[{Type}]: {Count} entries have been removed", result.Completed.ToString(), - result.Scope.ToString()); + _logger.LogInformation("[{Type}]: {Count} entries have been removed", result.Scope.ToString(), + result.Completed.ToString() + ); } } } \ No newline at end of file diff --git a/src/AnimeFeedManager.Web/Features/Admin/Admin.razor b/src/AnimeFeedManager.Web/Features/Admin/Admin.razor index 67ddb0a6..304b46b6 100644 --- a/src/AnimeFeedManager.Web/Features/Admin/Admin.razor +++ b/src/AnimeFeedManager.Web/Features/Admin/Admin.razor @@ -24,6 +24,12 @@ +
+
+

Users

+
+ +
diff --git a/src/AnimeFeedManager.Web/Features/Admin/AdminSeriesCard.razor b/src/AnimeFeedManager.Web/Features/Admin/AdminSeriesCard.razor index 1518e832..952e77ec 100644 --- a/src/AnimeFeedManager.Web/Features/Admin/AdminSeriesCard.razor +++ b/src/AnimeFeedManager.Web/Features/Admin/AdminSeriesCard.razor @@ -11,11 +11,7 @@ -
@@ -37,9 +33,8 @@ logger, + CancellationToken token) => payload.Parse() + .BindAsync(data => subscriptionsCopier.StartCopyProcess(data.Source, data.Target, token)) + .ToComponentResult(renderer, logger, "Copy of subscription will be processed in the background") + ).RequireAuthorization(Policies.AdminRequired); + + app.MapPut("admin/user/delete", + ([FromForm] string source, + [FromServices] BlazorRenderer renderer, + [FromServices] IUserDelete userDeleter, + [FromServices] ILogger logger, + CancellationToken token) => UserIdValidator.Validate(source) + .ValidationToEither() + .BindAsync(userId => userDeleter.Delete(userId, token)) + .ToComponentResult(renderer, logger, "User has been deleted from the system. Subscriptions will be processed in the background") + ).RequireAuthorization(Policies.AdminRequired); + app.MapPut("/admin/noop", async (BlazorRenderer renderer) => { var parameters = new Dictionary diff --git a/src/AnimeFeedManager.Web/Features/Admin/RenderFragments.cs b/src/AnimeFeedManager.Web/Features/Admin/RenderFragments.cs new file mode 100644 index 00000000..0d6b9dad --- /dev/null +++ b/src/AnimeFeedManager.Web/Features/Admin/RenderFragments.cs @@ -0,0 +1,44 @@ +using AnimeFeedManager.Features.Users.Types; +using AnimeFeedManager.Web.Features.Common; + +namespace AnimeFeedManager.Web.Features.Admin; + +internal static class RenderFragments +{ + internal static async Task ToComponentResult( + this Task> result, + BlazorRenderer renderer, + ILogger logger, + string okMessage) + { + var r = await result; + + var response = await r.Match( + check => RenderOk(renderer, check, okMessage, logger), + error => Common.DefaultResponses.Extensions.RenderError(renderer, logger, error) + ); + return Results.Content(response.Html, "text/html"); + } + + private static Task RenderOk(BlazorRenderer renderer, UsersCheck result, string message, ILogger logger) + { + return result switch + { + AllMatched => Common.DefaultResponses.Extensions.RenderOk(renderer, message), + SomeNotFound s => RenderUserNotFound(renderer, s), + _ => Common.DefaultResponses.Extensions.RenderError(renderer, logger, + BasicError.Create($"{nameof(UsersCheck)} is out of range")) + }; + } + + + private static Task RenderUserNotFound(BlazorRenderer renderer, SomeNotFound notFound) + { + var parameters = new Dictionary + { + {nameof(UserNotFoundResult.Condition), notFound} + }; + + return renderer.RenderComponent(parameters); + } +} \ No newline at end of file diff --git a/src/AnimeFeedManager.Web/Features/Admin/Types.cs b/src/AnimeFeedManager.Web/Features/Admin/Types.cs new file mode 100644 index 00000000..a995fc15 --- /dev/null +++ b/src/AnimeFeedManager.Web/Features/Admin/Types.cs @@ -0,0 +1,14 @@ +using AnimeFeedManager.Common.Utils; + +namespace AnimeFeedManager.Web.Features.Admin; + +public record CopyUserPayload(string Source, string Target); + +public static class Extensions +{ + public static Either Parse(this CopyUserPayload payload) + { + return (UserIdValidator.Validate(payload.Source), UserIdValidator.Validate(payload.Target)) + .Apply((s, t) => (s, t)).ValidationToEither(); + } +} \ No newline at end of file diff --git a/src/AnimeFeedManager.Web/Features/Admin/UserNotFoundResult.razor b/src/AnimeFeedManager.Web/Features/Admin/UserNotFoundResult.razor new file mode 100644 index 00000000..e6ada7c6 --- /dev/null +++ b/src/AnimeFeedManager.Web/Features/Admin/UserNotFoundResult.razor @@ -0,0 +1,18 @@ +@using AnimeFeedManager.Features.Users.Types +@using System.Collections.Immutable +
+ +
+ +@code { + [Parameter, EditorRequired] public SomeNotFound Condition { get; set; } = new(ImmutableList.Empty); +} \ No newline at end of file diff --git a/src/AnimeFeedManager.Web/Features/Admin/Users.razor b/src/AnimeFeedManager.Web/Features/Admin/Users.razor new file mode 100644 index 00000000..7f7ea6b8 --- /dev/null +++ b/src/AnimeFeedManager.Web/Features/Admin/Users.razor @@ -0,0 +1,65 @@ +
+ + + + +
+ + + + +
+ + + +
+ + +
+ + + + + +
+
\ No newline at end of file diff --git a/src/AnimeFeedManager.Web/Features/Common/DefaultResponses/Extensions.cs b/src/AnimeFeedManager.Web/Features/Common/DefaultResponses/Extensions.cs index c2414176..898ccb2f 100644 --- a/src/AnimeFeedManager.Web/Features/Common/DefaultResponses/Extensions.cs +++ b/src/AnimeFeedManager.Web/Features/Common/DefaultResponses/Extensions.cs @@ -41,7 +41,7 @@ private static async Task RenderOk(BlazorRenderer renderer, s return messageComponent.Combine(additionalComponents); } - private static Task RenderOk(BlazorRenderer renderer, string message) + internal static Task RenderOk(BlazorRenderer renderer, string message) { var parameters = new Dictionary { @@ -51,7 +51,7 @@ private static Task RenderOk(BlazorRenderer renderer, string return renderer.RenderComponent(parameters); } - private static Task RenderError(BlazorRenderer renderer, ILogger logger, DomainError error) + internal static Task RenderError(BlazorRenderer renderer, ILogger logger, DomainError error) { error.LogError(logger); diff --git a/src/AnimeFeedManager.Web/wwwroot/app.css b/src/AnimeFeedManager.Web/wwwroot/app.css index 8c47ab57..b0315074 100644 --- a/src/AnimeFeedManager.Web/wwwroot/app.css +++ b/src/AnimeFeedManager.Web/wwwroot/app.css @@ -1228,6 +1228,18 @@ html { } } + .btn-outline.btn-error:hover { + --tw-text-opacity: 1; + color: var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity))); + } + + @supports (color: color-mix(in oklab, black, black)) { + .btn-outline.btn-error:hover { + background-color: color-mix(in oklab, var(--fallback-er,oklch(var(--er)/1)) 90%, black); + border-color: color-mix(in oklab, var(--fallback-er,oklch(var(--er)/1)) 90%, black); + } + } + .btn-disabled:hover, .btn[disabled]:hover, .btn:disabled:hover { @@ -1792,6 +1804,10 @@ html { .btn-secondary { --btn-color: var(--fallback-s); } + + .btn-error { + --btn-color: var(--fallback-er); + } } @supports (color: color-mix(in oklab, black, black)) { @@ -1804,6 +1820,11 @@ html { background-color: color-mix(in oklab, var(--fallback-s,oklch(var(--s)/1)) 90%, black); border-color: color-mix(in oklab, var(--fallback-s,oklch(var(--s)/1)) 90%, black); } + + .btn-outline.btn-error.btn-active { + background-color: color-mix(in oklab, var(--fallback-er,oklch(var(--er)/1)) 90%, black); + border-color: color-mix(in oklab, var(--fallback-er,oklch(var(--er)/1)) 90%, black); + } } .btn:focus-visible { @@ -1826,6 +1847,10 @@ html { .btn-secondary { --btn-color: var(--s); } + + .btn-error { + --btn-color: var(--er); + } } .btn-secondary { @@ -1834,6 +1859,12 @@ html { outline-color: var(--fallback-s,oklch(var(--s)/1)); } +.btn-error { + --tw-text-opacity: 1; + color: var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity))); + outline-color: var(--fallback-er,oklch(var(--er)/1)); +} + .btn.glass { --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; @@ -1882,6 +1913,16 @@ html { color: var(--fallback-sc,oklch(var(--sc)/var(--tw-text-opacity))); } +.btn-outline.btn-error { + --tw-text-opacity: 1; + color: var(--fallback-er,oklch(var(--er)/var(--tw-text-opacity))); +} + +.btn-outline.btn-error.btn-active { + --tw-text-opacity: 1; + color: var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity))); +} + .btn.btn-disabled, .btn[disabled], .btn:disabled { @@ -2067,18 +2108,6 @@ html { outline-color: var(--fallback-bc,oklch(var(--bc)/0.2)); } -.input-error { - --tw-border-opacity: 1; - border-color: var(--fallback-er,oklch(var(--er)/var(--tw-border-opacity))); -} - -.input-error:focus, - .input-error:focus-within { - --tw-border-opacity: 1; - border-color: var(--fallback-er,oklch(var(--er)/var(--tw-border-opacity))); - outline-color: var(--fallback-er,oklch(var(--er)/1)); -} - .input-disabled, .input:disabled, .input[disabled] { @@ -3311,149 +3340,10 @@ html { content: "An error has occurred." } -.required\:\!input:required { - flex-shrink: 1 !important; - -webkit-appearance: none !important; - -moz-appearance: none !important; - appearance: none !important; - height: 3rem !important; - padding-left: 1rem !important; - padding-right: 1rem !important; - font-size: 1rem !important; - line-height: 2 !important; - line-height: 1.5rem !important; - border-radius: var(--rounded-btn, 0.5rem) !important; - border-width: 1px !important; - border-color: transparent !important; - --tw-bg-opacity: 1 !important; - background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity))) !important; -} - -.required\:\!input:required input:focus { - outline: 2px solid transparent !important; - outline-offset: 2px !important; -} - -.required\:\!input:required[list]::-webkit-calendar-picker-indicator { - line-height: 1em !important; -} - .required\:\!input-bordered:required { border-color: var(--fallback-bc,oklch(var(--bc)/0.2)) !important; } -.required\:\!input:required:focus,.required\:\!input:required:focus-within { - box-shadow: none !important; - border-color: var(--fallback-bc,oklch(var(--bc)/0.2)) !important; - outline-style: solid !important; - outline-width: 2px !important; - outline-offset: 2px !important; - outline-color: var(--fallback-bc,oklch(var(--bc)/0.2)) !important; -} - -.required\:input-info:required { - --tw-border-opacity: 1; - border-color: var(--fallback-in,oklch(var(--in)/var(--tw-border-opacity))); -} - -.required\:\!input-info:required { - --tw-border-opacity: 1 !important; - border-color: var(--fallback-in,oklch(var(--in)/var(--tw-border-opacity))) !important; -} - -.required\:input-info:required:focus,.required\:input-info:required:focus-within { - --tw-border-opacity: 1; - border-color: var(--fallback-in,oklch(var(--in)/var(--tw-border-opacity))); - outline-color: var(--fallback-in,oklch(var(--in)/1)); -} - -.required\:\!input-info:required:focus,.required\:\!input-info:required:focus-within { - --tw-border-opacity: 1 !important; - border-color: var(--fallback-in,oklch(var(--in)/var(--tw-border-opacity))) !important; - outline-color: var(--fallback-in,oklch(var(--in)/1)) !important; -} - -.required\:input-info:required:focus,.required\:input-info:required:focus-within { - --tw-border-opacity: 1; - border-color: var(--fallback-in,oklch(var(--in)/var(--tw-border-opacity))); - outline-color: var(--fallback-in,oklch(var(--in)/1)); -} - -.required\:\!input-info:required:focus,.required\:\!input-info:required:focus-within { - --tw-border-opacity: 1 !important; - border-color: var(--fallback-in,oklch(var(--in)/var(--tw-border-opacity))) !important; - outline-color: var(--fallback-in,oklch(var(--in)/1)) !important; -} - -.required\:\!input:required:disabled,.required\:\!input:required[disabled] { - cursor: not-allowed !important; - --tw-border-opacity: 1 !important; - border-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity))) !important; - --tw-bg-opacity: 1 !important; - background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity))) !important; - color: var(--fallback-bc,oklch(var(--bc)/0.4)) !important; -} - -.required\:\!input:required:disabled::-moz-placeholder, .required\:\!input:required[disabled]::-moz-placeholder { - color: var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity))) !important; - --tw-placeholder-opacity: 0.2 !important; -} - -.required\:\!input:required:disabled::placeholder,.required\:\!input:required[disabled]::placeholder { - color: var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity))) !important; - --tw-placeholder-opacity: 0.2 !important; -} - -.required\:\!input:required::-webkit-date-and-time-value { - text-align: inherit !important; -} - -.mockup-browser .mockup-browser-toolbar .required\:\!input:required { - position: relative !important; - margin-left: auto !important; - margin-right: auto !important; - display: block !important; - height: 1.75rem !important; - width: 24rem !important; - overflow: hidden !important; - text-overflow: ellipsis !important; - white-space: nowrap !important; - --tw-bg-opacity: 1 !important; - background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity))) !important; - padding-left: 2rem !important; - direction: ltr !important; -} - -.mockup-browser .mockup-browser-toolbar .required\:\!input:required:before { - content: "" !important; - position: absolute !important; - left: 0.5rem !important; - top: 50% !important; - aspect-ratio: 1 / 1 !important; - height: 0.75rem !important; - --tw-translate-y: -50% !important; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)) !important; - border-radius: 9999px !important; - border-width: 2px !important; - border-color: currentColor !important; - opacity: 0.6 !important; -} - -.mockup-browser .mockup-browser-toolbar .required\:\!input:required:after { - content: "" !important; - position: absolute !important; - left: 1.25rem !important; - top: 50% !important; - height: 0.5rem !important; - --tw-translate-y: 25% !important; - --tw-rotate: -45deg !important; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)) !important; - border-radius: 9999px !important; - border-width: 1px !important; - border-color: currentColor !important; - opacity: 0.6 !important; -} - .invalid\:input-error:invalid { --tw-border-opacity: 1; border-color: var(--fallback-er,oklch(var(--er)/var(--tw-border-opacity)));