Merge branch 'p6-w1-t1-cms-dashboard': CMS home dashboard
This commit is contained in:
@@ -1,13 +1,125 @@
|
|||||||
@page "/"
|
@page "/"
|
||||||
|
@using DeepDrftManager.Services
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
@layout Layout.CmsLayout
|
@layout Layout.CmsLayout
|
||||||
@inject NavigationManager Nav
|
@inject NavigationManager Nav
|
||||||
|
@inject ICmsTrackService CmsTrackService
|
||||||
|
@inject ILogger<Index> Logger
|
||||||
|
|
||||||
<PageTitle>DeepDrft CMS</PageTitle>
|
<PageTitle>DeepDrft CMS</PageTitle>
|
||||||
|
|
||||||
|
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-8">
|
||||||
|
<MudText Typo="Typo.h3" Class="mb-6">Catalogue</MudText>
|
||||||
|
|
||||||
|
<MudGrid Spacing="4">
|
||||||
|
<MudItem xs="12" sm="4">
|
||||||
|
@SummaryCard("Tracks", Icons.Material.Filled.LibraryMusic, Color.Primary, _tracksLoading, _trackCount)
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" sm="4">
|
||||||
|
@SummaryCard("Albums", Icons.Material.Filled.Album, Color.Secondary, _albumsLoading, _albumCount)
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" sm="4">
|
||||||
|
@SummaryCard("Genres", Icons.Material.Filled.Category, Color.Tertiary, _genresLoading, _genreCount)
|
||||||
|
</MudItem>
|
||||||
|
</MudGrid>
|
||||||
|
</MudContainer>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
protected override void OnInitialized()
|
private bool _tracksLoading = true;
|
||||||
|
private bool _albumsLoading = true;
|
||||||
|
private bool _genresLoading = true;
|
||||||
|
|
||||||
|
private int? _trackCount;
|
||||||
|
private int? _albumCount;
|
||||||
|
private int? _genreCount;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
Nav.NavigateTo("/tracks");
|
// Three independent reads run concurrently. Each loader calls StateHasChanged in its
|
||||||
|
// finally block so its card updates as soon as its own fetch returns.
|
||||||
|
await Task.WhenAll(LoadTrackCount(), LoadAlbumCount(), LoadGenreCount());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task LoadTrackCount()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await CmsTrackService.GetTrackCountAsync();
|
||||||
|
_trackCount = result.Success ? result.Value : null;
|
||||||
|
if (!result.Success)
|
||||||
|
{
|
||||||
|
Logger.LogWarning("Dashboard track count failed: {Error}",
|
||||||
|
result.Messages.FirstOrDefault()?.Message ?? "Unknown error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_tracksLoading = false;
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadAlbumCount()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await CmsTrackService.GetAlbumSummariesAsync();
|
||||||
|
_albumCount = result.Success && result.Value is not null ? result.Value.Count : null;
|
||||||
|
if (!result.Success)
|
||||||
|
{
|
||||||
|
Logger.LogWarning("Dashboard album summaries failed: {Error}",
|
||||||
|
result.Messages.FirstOrDefault()?.Message ?? "Unknown error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_albumsLoading = false;
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadGenreCount()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await CmsTrackService.GetGenreSummariesAsync();
|
||||||
|
_genreCount = result.Success && result.Value is not null ? result.Value.Count : null;
|
||||||
|
if (!result.Success)
|
||||||
|
{
|
||||||
|
Logger.LogWarning("Dashboard genre summaries failed: {Error}",
|
||||||
|
result.Messages.FirstOrDefault()?.Message ?? "Unknown error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_genresLoading = false;
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private RenderFragment SummaryCard(string label, string icon, Color color, bool loading, int? count) => __builder =>
|
||||||
|
{
|
||||||
|
<MudCard Elevation="8" Style="height: 100%;">
|
||||||
|
<MudCardContent>
|
||||||
|
<MudStack AlignItems="AlignItems.Center" Spacing="2" Class="py-4">
|
||||||
|
<MudIcon Icon="@icon" Color="@color" Size="Size.Large" />
|
||||||
|
@if (loading)
|
||||||
|
{
|
||||||
|
<MudProgressCircular Color="@color" Indeterminate="true" Size="Size.Small" />
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.h3" Color="@color">@(count?.ToString() ?? "—")</MudText>
|
||||||
|
}
|
||||||
|
<MudText Typo="Typo.subtitle1" Class="mud-text-secondary text-uppercase">@label</MudText>
|
||||||
|
</MudStack>
|
||||||
|
</MudCardContent>
|
||||||
|
<MudCardActions Class="justify-center pb-4">
|
||||||
|
<MudButton Variant="Variant.Text" Color="@color" EndIcon="@Icons.Material.Filled.ArrowForward"
|
||||||
|
OnClick="@(() => Nav.NavigateTo("/tracks"))">
|
||||||
|
View
|
||||||
|
</MudButton>
|
||||||
|
</MudCardActions>
|
||||||
|
</MudCard>
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -440,4 +440,106 @@ public class CmsTrackService : ICmsTrackService
|
|||||||
return Result.CreateFailResult("Failed to generate waveform profile.");
|
return Result.CreateFailResult("Failed to generate waveform profile.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<ResultContainer<List<AlbumSummaryDto>>> GetAlbumSummariesAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var client = _httpClientFactory.CreateClient(ContentCmsClientName);
|
||||||
|
|
||||||
|
HttpResponseMessage response;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
response = await client.GetAsync("api/track/albums", ct);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Content API call failed for album summaries");
|
||||||
|
return ResultContainer<List<AlbumSummaryDto>>.CreateFailResult("Content API is unreachable.");
|
||||||
|
}
|
||||||
|
|
||||||
|
using (response)
|
||||||
|
{
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
_logger.LogError("Content API album summaries failed: {Status}", (int)response.StatusCode);
|
||||||
|
return ResultContainer<List<AlbumSummaryDto>>.CreateFailResult("Failed to load albums.");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<AlbumSummaryDto>? albums;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
albums = await response.Content.ReadFromJsonAsync<List<AlbumSummaryDto>>(ct);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to deserialize album summaries from Content API response");
|
||||||
|
return ResultContainer<List<AlbumSummaryDto>>.CreateFailResult("Content API returned an unexpected response.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (albums is null)
|
||||||
|
{
|
||||||
|
_logger.LogError("Content API returned a null album summaries list");
|
||||||
|
return ResultContainer<List<AlbumSummaryDto>>.CreateFailResult("Content API returned an empty response.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResultContainer<List<AlbumSummaryDto>>.CreatePassResult(albums);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ResultContainer<List<GenreSummaryDto>>> GetGenreSummariesAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var client = _httpClientFactory.CreateClient(ContentCmsClientName);
|
||||||
|
|
||||||
|
HttpResponseMessage response;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
response = await client.GetAsync("api/track/genres", ct);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Content API call failed for genre summaries");
|
||||||
|
return ResultContainer<List<GenreSummaryDto>>.CreateFailResult("Content API is unreachable.");
|
||||||
|
}
|
||||||
|
|
||||||
|
using (response)
|
||||||
|
{
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
_logger.LogError("Content API genre summaries failed: {Status}", (int)response.StatusCode);
|
||||||
|
return ResultContainer<List<GenreSummaryDto>>.CreateFailResult("Failed to load genres.");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<GenreSummaryDto>? genres;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
genres = await response.Content.ReadFromJsonAsync<List<GenreSummaryDto>>(ct);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to deserialize genre summaries from Content API response");
|
||||||
|
return ResultContainer<List<GenreSummaryDto>>.CreateFailResult("Content API returned an unexpected response.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (genres is null)
|
||||||
|
{
|
||||||
|
_logger.LogError("Content API returned a null genre summaries list");
|
||||||
|
return ResultContainer<List<GenreSummaryDto>>.CreateFailResult("Content API returned an empty response.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResultContainer<List<GenreSummaryDto>>.CreatePassResult(genres);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ResultContainer<int>> GetTrackCountAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
// Re-use the paged endpoint: a single-item page carries the full TotalCount, so no
|
||||||
|
// dedicated count endpoint is needed.
|
||||||
|
var paged = await GetPagedAsync(page: 1, pageSize: 1, sortColumn: null, sortDescending: false, ct);
|
||||||
|
if (!paged.Success || paged.Value is null)
|
||||||
|
{
|
||||||
|
var error = paged.Messages.FirstOrDefault()?.Message ?? "Failed to load track count.";
|
||||||
|
return ResultContainer<int>.CreateFailResult(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResultContainer<int>.CreatePassResult(paged.Value.TotalCount);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,4 +82,15 @@ public interface ICmsTrackService
|
|||||||
/// <c>POST api/track/{entryKey}/waveform</c>. Maps a 404 to a "Track audio not found." failure.
|
/// <c>POST api/track/{entryKey}/waveform</c>. Maps a 404 to a "Track audio not found." failure.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<Result> GenerateWaveformProfileAsync(string entryKey, CancellationToken ct = default);
|
Task<Result> GenerateWaveformProfileAsync(string entryKey, CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>Returns all distinct albums with track counts from GET api/track/albums.</summary>
|
||||||
|
Task<ResultContainer<List<AlbumSummaryDto>>> GetAlbumSummariesAsync(CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>Returns all distinct genres with track counts from GET api/track/genres.</summary>
|
||||||
|
Task<ResultContainer<List<GenreSummaryDto>>> GetGenreSummariesAsync(CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the total track count by calling GET api/track/page with pageSize=1 and reading TotalCount.
|
||||||
|
/// </summary>
|
||||||
|
Task<ResultContainer<int>> GetTrackCountAsync(CancellationToken ct = default);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user