Skip to content

Commit

Permalink
📐 Adding copy subscriptions and user deletion to UI
Browse files Browse the repository at this point in the history
🩹 Fixing issues
  • Loading branch information
xxnickles committed Jan 28, 2024
1 parent 7e669c6 commit 7b53e7b
Show file tree
Hide file tree
Showing 13 changed files with 233 additions and 170 deletions.
12 changes: 9 additions & 3 deletions src/AnimeFeedManager.Features/Users/IO/UserVerification.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,22 @@ public Task<Either<DomainError, UsersCheck>> CheckUsersExist(CancellationToken t
.BindAsync(client =>
TableUtils.ExecuteQueryWithNotFound(() =>
client.QueryAsync<UserStorage>(
u => u.PartitionKey == Constants.UserPartitionKey && usersString.Contains(u.RowKey),
GetUserFilter(usersString),
cancellationToken: token)))
.MapAsync(matches => ExtractResults(matches, usersString));
}

private UsersCheck ExtractResults(ImmutableList<UserStorage> matches, IEnumerable<string> targets)
private static UsersCheck ExtractResults(ImmutableList<UserStorage> matches, IEnumerable<string> 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<string> users)
{
return users.Aggregate(TableClient.CreateQueryFilter($"PartitionKey eq {Constants.UserPartitionKey}"),
(current, user) => current + TableClient.CreateQueryFilter($" or RowKey eq {user}"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ public abstract record UsersCheck;

public record AllMatched : UsersCheck;

public record SomeNotFound(ImmutableList<string> notFoundUsers): UsersCheck;
public record SomeNotFound(ImmutableList<string> NotFoundUsers): UsersCheck;
8 changes: 4 additions & 4 deletions src/AnimeFeedManager.Functions/Users/OnCopySubscriptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -40,8 +40,8 @@ private void LogResults(ImmutableList<ProcessResult> 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());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ private void LogResults(ImmutableList<ProcessResult> 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()
);
}
}
}
6 changes: 6 additions & 0 deletions src/AnimeFeedManager.Web/Features/Admin/Admin.razor
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@
<AdminSeriesCard SeriesType="SeriesType.Movie"></AdminSeriesCard>
</section>

<section class="col-span-12 px-5 pt-7.5 pb-5 sm:px-7.5 lg:col-span-4">
<header class="mb-4">
<h2>Users</h2>
</header>
<Users></Users>
</section>

<section class="col-span-12 px-5 pt-7.5 pb-5 sm:px-7.5 lg:col-span-4">
<header class="mb-4">
Expand Down
9 changes: 2 additions & 7 deletions src/AnimeFeedManager.Web/Features/Admin/AdminSeriesCard.razor
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,7 @@
</AdminCard>

<AdminCard Title="Update by Season">
<form x-data="{
valid: true ,
get isValid() { return this.valid }
}"
hx-put="@GetSeasonPath()"
<form hx-put="@GetSeasonPath()"
hx-swap="none">
<AntiforgeryToken/>

Expand All @@ -37,9 +33,8 @@

<input name="year"
type="number"
class="input" :class="isValid || 'input-bordered input-error'"
class="input input-bordered invalid:input-error required:!input-bordered"
placeholder="Input year"
x-on:input.debounce="valid = $event.target.validity.valid"
required
min="@_minYear.ToString()"
max="@_maxYear.ToString()"
Expand Down
24 changes: 24 additions & 0 deletions src/AnimeFeedManager.Web/Features/Admin/Endpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
using AnimeFeedManager.Common.Domain.Events;
using AnimeFeedManager.Common.Domain.Validators;
using AnimeFeedManager.Common.Dto;
using AnimeFeedManager.Common.Utils;
using AnimeFeedManager.Features.Infrastructure.Messaging;
using AnimeFeedManager.Features.Users;
using AnimeFeedManager.Features.Users.IO;
using AnimeFeedManager.Web.Features.Common;
using AnimeFeedManager.Web.Features.Common.DefaultResponses;
using AnimeFeedManager.Web.Features.Security;
Expand Down Expand Up @@ -118,6 +121,27 @@ public static void Map(WebApplication app)
"Latest Titles will be processed in the background"))
.RequireAuthorization(Policies.AdminRequired);

app.MapPut("admin/user/copy",
([FromForm] CopyUserPayload payload,
[FromServices] BlazorRenderer renderer,
[FromServices] SubscriptionCopierSetter subscriptionsCopier,
[FromServices] ILogger<Admin> 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<Admin> 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<string, object?>
Expand Down
44 changes: 44 additions & 0 deletions src/AnimeFeedManager.Web/Features/Admin/RenderFragments.cs
Original file line number Diff line number Diff line change
@@ -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<IResult> ToComponentResult(
this Task<Either<DomainError, UsersCheck>> 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<RenderedComponent> 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<RenderedComponent> RenderUserNotFound(BlazorRenderer renderer, SomeNotFound notFound)
{
var parameters = new Dictionary<string, object?>
{
{nameof(UserNotFoundResult.Condition), notFound}
};

return renderer.RenderComponent<UserNotFoundResult>(parameters);
}
}
14 changes: 14 additions & 0 deletions src/AnimeFeedManager.Web/Features/Admin/Types.cs
Original file line number Diff line number Diff line change
@@ -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<DomainError, (UserId Source, UserId Target)> Parse(this CopyUserPayload payload)
{
return (UserIdValidator.Validate(payload.Source), UserIdValidator.Validate(payload.Target))
.Apply((s, t) => (s, t)).ValidationToEither();
}
}
18 changes: 18 additions & 0 deletions src/AnimeFeedManager.Web/Features/Admin/UserNotFoundResult.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
@using AnimeFeedManager.Features.Users.Types
@using System.Collections.Immutable
<div hx-swap-oob="afterbegin:#toast-panel">
<div role="alert" class="alert alert-error" x-data="{ open: true }" x-show="open" x-transition.duration.500ms>
<p class="text-sm whitespace-normal">
Following users are not registered in the system: @string.Join(", ", Condition.NotFoundUsers)
</p>
<button type="button" class="btn btn-circle btn-ghost btn-xs" x-on:click="open = false">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>`
</button>
</div>
</div>

@code {
[Parameter, EditorRequired] public SomeNotFound Condition { get; set; } = new(ImmutableList<string>.Empty);
}
65 changes: 65 additions & 0 deletions src/AnimeFeedManager.Web/Features/Admin/Users.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<div class="flex flex-wrap gap-3">
<AdminCard Title="Copy Subscriptions">
<form class="card-actions form-control"
hx-put="/admin/user/copy"
hx-swap="none"
:hx-confirm="confirmationMessage"
x-data="{ source:'', target:'',
get confirmationMessage() { return `Do you want to copy series from ${this.source} to ${this.target}?`}
}">
<AntiforgeryToken/>

<fieldset class="form-control w-full">
<label class="label" for="copy-source-id">
<span class="label-text">Source User Id</span>
</label>
<input name="source"
id="copy-source-id"
type="text"
class="input input-bordered w-full invalid:input-error required:!input-bordered"
placeholder="Source User Id"
required
x-model="source"/>
<label class="label" for="copy-target-id">
<span class="label-text">Target User Id</span>
</label>
<input name="target"
type="text"
id="copy-target-id"
class="input input-bordered w-full invalid:input-error required:!input-bordered"
placeholder="Target User Id"
required
x-model="target"/>
</fieldset>
<button type="submit" class="btn btn-primary mt-4">
Copy Subscriptions
</button>

</form>
</AdminCard>

<AdminCard Title="Delete User">
<form class="card-actions form-control"
hx-put="/admin/user/delete"
hx-swap="none"
:hx-confirm="confirmationMessage"
x-data="{ source:'',
get confirmationMessage() { return `Do you want to delete ${this.source} and its associated data?`}
}">
<AntiforgeryToken/>
<label class="label" for="delete-source-id">
<span class="label-text">Source User Id</span>
</label>
<input name="source"
id="delete-source-id"
type="text"
class="input input-bordered invalid:input-error required:!input-bordered w-full"
placeholder="Source User Id"
required
x-model="source"/>
<button type="submit" class="btn btn-error mt-4">
Delete User
</button>
</form>
</AdminCard>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ private static async Task<RenderedComponent> RenderOk(BlazorRenderer renderer, s
return messageComponent.Combine(additionalComponents);
}

private static Task<RenderedComponent> RenderOk(BlazorRenderer renderer, string message)
internal static Task<RenderedComponent> RenderOk(BlazorRenderer renderer, string message)
{
var parameters = new Dictionary<string, object?>
{
Expand All @@ -51,7 +51,7 @@ private static Task<RenderedComponent> RenderOk(BlazorRenderer renderer, string
return renderer.RenderComponent<OkResult>(parameters);
}

private static Task<RenderedComponent> RenderError(BlazorRenderer renderer, ILogger logger, DomainError error)
internal static Task<RenderedComponent> RenderError(BlazorRenderer renderer, ILogger logger, DomainError error)
{
error.LogError(logger);

Expand Down
Loading

0 comments on commit 7b53e7b

Please sign in to comment.