Browse Source

table improvements (#272)

* log viewer improvements

* playout detail table improvements

* schedule items table improvements

* remove schedule items pager
pull/273/head
Jason Dove 4 years ago committed by GitHub
parent
commit
cebab33d79
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      CHANGELOG.md
  2. 6
      ErsatzTV.Application/Logs/PagedLogEntriesViewModel.cs
  3. 11
      ErsatzTV.Application/Logs/Queries/GetRecentLogEntries.cs
  4. 38
      ErsatzTV.Application/Logs/Queries/GetRecentLogEntriesHandler.cs
  5. 6
      ErsatzTV.Application/Playouts/PagedPlayoutItemsViewModel.cs
  6. 2
      ErsatzTV.Application/Playouts/Queries/GetPlayoutItemsById.cs
  7. 18
      ErsatzTV.Application/Playouts/Queries/GetPlayoutItemsByIdHandler.cs
  8. 3
      ErsatzTV.Core/Domain/ConfigElementKey.cs
  9. 11
      ErsatzTV.Core/Interfaces/Repositories/ILogRepository.cs
  10. 19
      ErsatzTV.Infrastructure/Data/Repositories/LogRepository.cs
  11. 55
      ErsatzTV/Pages/Logs.razor
  12. 36
      ErsatzTV/Pages/Playouts.razor
  13. 3
      ErsatzTV/Pages/ScheduleItemsEditor.razor
  14. 35
      ErsatzTV/Pages/Schedules.razor
  15. 15
      ErsatzTV/Startup.cs

7
CHANGELOG.md

@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
### Added
- Add warning during playout rebuild when schedule has been emptied
- Save Logs, Playout Detail, Schedule Detail table page sizes
### Changed
- Show all log entries in log viewer, not just most recent 100 entries
- Use server-side paging and sorting for Logs table
- Use server-side paging for Playout Detail table
- Remove pager from Schedule Items editor (all schedule items will always be displayed)
### Fixed
- Fix ui crash adding a channel without a watermark

6
ErsatzTV.Application/Logs/PagedLogEntriesViewModel.cs

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
using System.Collections.Generic;
namespace ErsatzTV.Application.Logs
{
public record PagedLogEntriesViewModel(int TotalCount, List<LogEntryViewModel> Page);
}

11
ErsatzTV.Application/Logs/Queries/GetRecentLogEntries.cs

@ -1,7 +1,14 @@ @@ -1,7 +1,14 @@
using System.Collections.Generic;
using System;
using System.Linq.Expressions;
using ErsatzTV.Core.Domain;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Logs.Queries
{
public record GetRecentLogEntries : IRequest<List<LogEntryViewModel>>;
public record GetRecentLogEntries(int PageNum, int PageSize) : IRequest<PagedLogEntriesViewModel>
{
public Expression<Func<LogEntry, object>> SortExpression { get; set; }
public Option<bool> SortDescending { get; set; }
}
}

38
ErsatzTV.Application/Logs/Queries/GetRecentLogEntriesHandler.cs

@ -2,20 +2,46 @@ @@ -2,20 +2,46 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Domain;
using ErsatzTV.Infrastructure.Data;
using LanguageExt;
using MediatR;
using Microsoft.EntityFrameworkCore;
using static ErsatzTV.Application.Logs.Mapper;
namespace ErsatzTV.Application.Logs.Queries
{
public class GetRecentLogEntriesHandler : IRequestHandler<GetRecentLogEntries, List<LogEntryViewModel>>
public class GetRecentLogEntriesHandler : IRequestHandler<GetRecentLogEntries, PagedLogEntriesViewModel>
{
private readonly ILogRepository _logRepository;
private readonly IDbContextFactory<LogContext> _dbContextFactory;
public GetRecentLogEntriesHandler(ILogRepository logRepository) => _logRepository = logRepository;
public GetRecentLogEntriesHandler(IDbContextFactory<LogContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public Task<List<LogEntryViewModel>> Handle(GetRecentLogEntries request, CancellationToken cancellationToken) =>
_logRepository.GetRecentLogEntries().Map(list => list.Map(ProjectToViewModel).ToList());
public async Task<PagedLogEntriesViewModel> Handle(
GetRecentLogEntries request,
CancellationToken cancellationToken)
{
await using LogContext logContext = _dbContextFactory.CreateDbContext();
int count = await logContext.LogEntries.CountAsync(cancellationToken);
IOrderedQueryable<LogEntry> ordered = logContext.LogEntries
.OrderByDescending(le => le.Id);
foreach (bool descending in request.SortDescending)
{
ordered = descending
? logContext.LogEntries.OrderByDescending(request.SortExpression).ThenByDescending(le => le.Id)
: logContext.LogEntries.OrderBy(request.SortExpression).ThenByDescending(le => le.Id);
}
List<LogEntryViewModel> page = await ordered
.Skip(request.PageNum * request.PageSize)
.Take(request.PageSize)
.ToListAsync(cancellationToken)
.Map(list => list.Map(ProjectToViewModel).ToList());
return new PagedLogEntriesViewModel(count, page);
}
}
}

6
ErsatzTV.Application/Playouts/PagedPlayoutItemsViewModel.cs

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
using System.Collections.Generic;
namespace ErsatzTV.Application.Playouts
{
public record PagedPlayoutItemsViewModel(int TotalCount, List<PlayoutItemViewModel> Page);
}

2
ErsatzTV.Application/Playouts/Queries/GetPlayoutItemsById.cs

@ -3,5 +3,5 @@ using MediatR; @@ -3,5 +3,5 @@ using MediatR;
namespace ErsatzTV.Application.Playouts.Queries
{
public record GetPlayoutItemsById(int PlayoutId) : IRequest<List<PlayoutItemViewModel>>;
public record GetPlayoutItemsById(int PlayoutId, int PageNum, int PageSize) : IRequest<PagedPlayoutItemsViewModel>;
}

18
ErsatzTV.Application/Playouts/Queries/GetPlayoutItemsByIdHandler.cs

@ -11,27 +11,29 @@ using static ErsatzTV.Application.Playouts.Mapper; @@ -11,27 +11,29 @@ using static ErsatzTV.Application.Playouts.Mapper;
namespace ErsatzTV.Application.Playouts.Queries
{
public class GetPlayoutItemsByIdHandler : IRequestHandler<GetPlayoutItemsById, List<PlayoutItemViewModel>>
public class GetPlayoutItemsByIdHandler : IRequestHandler<GetPlayoutItemsById, PagedPlayoutItemsViewModel>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public GetPlayoutItemsByIdHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public async Task<List<PlayoutItemViewModel>> Handle(
public async Task<PagedPlayoutItemsViewModel> Handle(
GetPlayoutItemsById request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
return await dbContext.PlayoutItems
int totalCount = await dbContext.PlayoutItems
.CountAsync(i => i.PlayoutId == request.PlayoutId, cancellationToken);
List<PlayoutItemViewModel> page = await dbContext.PlayoutItems
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as Movie).MovieMetadata)
.ThenInclude(mm => mm.Artwork)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as Movie).MediaVersions)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as MusicVideo).MusicVideoMetadata)
.ThenInclude(mm => mm.Artwork)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as MusicVideo).MediaVersions)
.Include(i => i.MediaItem)
@ -39,7 +41,6 @@ namespace ErsatzTV.Application.Playouts.Queries @@ -39,7 +41,6 @@ namespace ErsatzTV.Application.Playouts.Queries
.ThenInclude(mm => mm.ArtistMetadata)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as Episode).EpisodeMetadata)
.ThenInclude(em => em.Artwork)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as Episode).MediaVersions)
.Include(i => i.MediaItem)
@ -49,8 +50,13 @@ namespace ErsatzTV.Application.Playouts.Queries @@ -49,8 +50,13 @@ namespace ErsatzTV.Application.Playouts.Queries
.ThenInclude(mi => (mi as Episode).Season.Show)
.ThenInclude(s => s.ShowMetadata)
.Filter(i => i.PlayoutId == request.PlayoutId)
.OrderBy(i => i.Start)
.Skip(request.PageNum * request.PageSize)
.Take(request.PageSize)
.ToListAsync(cancellationToken)
.Map(list => list.Map(ProjectToViewModel).ToList());
return new PagedPlayoutItemsViewModel(totalCount, page);
}
}
}

3
ErsatzTV.Core/Domain/ConfigElementKey.cs

@ -18,7 +18,10 @@ @@ -18,7 +18,10 @@
public static ConfigElementKey ChannelsPageSize => new("pages.channels.page_size");
public static ConfigElementKey CollectionsPageSize => new("pages.collections.page_size");
public static ConfigElementKey SchedulesPageSize => new("pages.schedules.page_size");
public static ConfigElementKey SchedulesDetailPageSize => new("pages.schedules.detail_page_size");
public static ConfigElementKey PlayoutsPageSize => new("pages.playouts.page_size");
public static ConfigElementKey PlayoutsDetailPageSize => new("pages.playouts.detail_page_size");
public static ConfigElementKey LogsPageSize => new("pages.logs.page_size");
public static ConfigElementKey LibraryRefreshInterval => new("scanner.library_refresh_interval");
}
}

11
ErsatzTV.Core/Interfaces/Repositories/ILogRepository.cs

@ -1,11 +0,0 @@ @@ -1,11 +0,0 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Core.Interfaces.Repositories
{
public interface ILogRepository
{
Task<List<LogEntry>> GetRecentLogEntries();
}
}

19
ErsatzTV.Infrastructure/Data/Repositories/LogRepository.cs

@ -1,19 +0,0 @@ @@ -1,19 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Infrastructure.Data.Repositories
{
public class LogRepository : ILogRepository
{
private readonly LogContext _logContext;
public LogRepository(LogContext logContext) => _logContext = logContext;
public Task<List<LogEntry>> GetRecentLogEntries() =>
_logContext.LogEntries.OrderByDescending(e => e.Id).Take(100).ToListAsync();
}
}

55
ErsatzTV/Pages/Logs.razor

@ -1,18 +1,24 @@ @@ -1,18 +1,24 @@
@page "/system/logs"
@using ErsatzTV.Application.Logs
@using ErsatzTV.Application.Logs.Queries
@using ErsatzTV.Application.Configuration.Queries
@using ErsatzTV.Application.Configuration.Commands
@inject IMediator _mediator
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudTable FixedHeader="true" Dense="true" Items="_logEntries">
<MudTable FixedHeader="true"
@bind-RowsPerPage="@_rowsPerPage"
ServerData="@(new Func<TableState, Task<TableData<LogEntryViewModel>>>(ServerReload))"
Dense="true"
@ref="_table">
<HeaderContent>
<MudTh>
<MudTableSortLabel SortBy="new Func<LogEntryViewModel, object>(x => x.Timestamp)">
<MudTableSortLabel T="LogEntryViewModel" SortLabel="Timestamp">
Timestamp
</MudTableSortLabel>
</MudTh>
<MudTh>
<MudTableSortLabel SortBy="new Func<LogEntryViewModel, object>(x => x.Level)">
<MudTableSortLabel T="LogEntryViewModel" SortLabel="Level">
Level
</MudTableSortLabel>
</MudTh>
@ -30,8 +36,47 @@ @@ -30,8 +36,47 @@
</MudContainer>
@code {
private List<LogEntryViewModel> _logEntries;
private MudTable<LogEntryViewModel> _table;
private int _rowsPerPage;
protected override async Task OnInitializedAsync() => _logEntries = await _mediator.Send(new GetRecentLogEntries());
protected override async Task OnParametersSetAsync() => _rowsPerPage = await _mediator.Send(new GetConfigElementByKey(ConfigElementKey.LogsPageSize))
.Map(maybeRows => maybeRows.Match(ce => int.TryParse(ce.Value, out int rows) ? rows : 10, () => 10));
private async Task<TableData<LogEntryViewModel>> ServerReload(TableState state)
{
await _mediator.Send(new SaveConfigElementByKey(ConfigElementKey.LogsPageSize, state.PageSize.ToString()));
PagedLogEntriesViewModel data;
switch (state.SortLabel?.ToLowerInvariant())
{
case "timestamp":
data = await _mediator.Send(new GetRecentLogEntries(state.Page, state.PageSize)
{
SortExpression = le => le.Timestamp,
SortDescending = state.SortDirection == SortDirection.None
? Option<bool>.None
: state.SortDirection == SortDirection.Descending
});
break;
case "level":
data = await _mediator.Send(new GetRecentLogEntries(state.Page, state.PageSize)
{
SortExpression = le => le.Level,
SortDescending = state.SortDirection == SortDirection.None
? Option<bool>.None
: state.SortDirection == SortDirection.Descending
});
break;
default:
data = await _mediator.Send(new GetRecentLogEntries(state.Page, state.PageSize)
{
SortDescending = Option<bool>.None
});
break;
}
return new TableData<LogEntryViewModel> { TotalItems = data.TotalCount, Items = data.Page };
}
}

36
ErsatzTV/Pages/Playouts.razor

@ -63,9 +63,14 @@ @@ -63,9 +63,14 @@
Add Playout
</MudButton>
@if (_selectedPlayoutItems != null)
@if (_selectedPlayoutId != null)
{
<MudTable Hover="true" Dense="true" Items="_selectedPlayoutItems.OrderBy(i => i.Start)" Class="mt-8">
<MudTable Class="mt-8"
Hover="true"
Dense="true"
@bind-RowsPerPage="@_detailRowsPerPage"
ServerData="@(new Func<TableState, Task<TableData<PlayoutItemViewModel>>>(DetailServerReload))"
@ref="_detailTable">
<ToolBarContent>
<MudText Typo="Typo.h6">Playout Detail</MudText>
</ToolBarContent>
@ -88,20 +93,23 @@ @@ -88,20 +93,23 @@
@code {
private MudTable<PlayoutNameViewModel> _table;
private MudTable<PlayoutItemViewModel> _detailTable;
private int _rowsPerPage;
private List<PlayoutItemViewModel> _selectedPlayoutItems;
private int _detailRowsPerPage;
private int? _selectedPlayoutId;
protected override async Task OnParametersSetAsync()
{
_rowsPerPage = await _mediator.Send(new GetConfigElementByKey(ConfigElementKey.PlayoutsPageSize))
.Map(maybeRows => maybeRows.Match(ce => int.TryParse(ce.Value, out int rows) ? rows : 10, () => 10));
_detailRowsPerPage = await _mediator.Send(new GetConfigElementByKey(ConfigElementKey.PlayoutsDetailPageSize))
.Map(maybeRows => maybeRows.Match(ce => int.TryParse(ce.Value, out int rows) ? rows : 10, () => 10));
}
private async Task PlayoutSelected(PlayoutNameViewModel playout)
{
_selectedPlayoutId = playout.PlayoutId;
_selectedPlayoutItems = await _mediator.Send(new GetPlayoutItemsById(playout.PlayoutId));
await _detailTable.ReloadServerData();
}
private async Task DeletePlayout(PlayoutNameViewModel playout)
@ -117,7 +125,7 @@ @@ -117,7 +125,7 @@
await _table.ReloadServerData();
if (_selectedPlayoutId == playout.PlayoutId)
{
_selectedPlayoutItems = null;
_selectedPlayoutId = null;
}
}
}
@ -147,4 +155,22 @@ @@ -147,4 +155,22 @@
};
}
private async Task<TableData<PlayoutItemViewModel>> DetailServerReload(TableState state)
{
await _mediator.Send(new SaveConfigElementByKey(ConfigElementKey.PlayoutsDetailPageSize, state.PageSize.ToString()));
if (_selectedPlayoutId.HasValue)
{
PagedPlayoutItemsViewModel data =
await _mediator.Send(new GetPlayoutItemsById(_selectedPlayoutId.Value, state.Page, state.PageSize));
return new TableData<PlayoutItemViewModel>
{
TotalItems = data.TotalCount,
Items = data.Page
};
}
return new TableData<PlayoutItemViewModel> { TotalItems = 0 };
}
}

3
ErsatzTV/Pages/ScheduleItemsEditor.razor

@ -71,9 +71,6 @@ @@ -71,9 +71,6 @@
</MudIconButton>
</MudTd>
</RowTemplate>
<PagerContent>
<MudTablePager/>
</PagerContent>
</MudTable>
<MudButton Variant="Variant.Filled" Color="Color.Default" OnClick="@(_ => AddScheduleItem())" Class="mt-4">
Add Schedule Item

35
ErsatzTV/Pages/Schedules.razor

@ -62,9 +62,13 @@ @@ -62,9 +62,13 @@
Add Schedule
</MudButton>
@if (_selectedScheduleItems != null)
@if (_selectedSchedule != null)
{
<MudTable Hover="true" Items="_selectedScheduleItems.OrderBy(i => i.Index)" Class="mt-8">
<MudTable Hover="true"
Class="mt-8"
@bind-RowsPerPage="@_detailRowsPerPage"
ServerData="@(new Func<TableState, Task<TableData<ProgramScheduleItemViewModel>>>(DetailServerReload))"
@ref="_detailTable">
<ToolBarContent>
<MudText Typo="Typo.h6">@_selectedSchedule.Name Items</MudText>
</ToolBarContent>
@ -89,21 +93,23 @@ @@ -89,21 +93,23 @@
@code {
private MudTable<ProgramScheduleViewModel> _table;
private MudTable<ProgramScheduleItemViewModel> _detailTable;
private int _rowsPerPage;
private List<ProgramScheduleItemViewModel> _selectedScheduleItems;
private int _detailRowsPerPage;
private ProgramScheduleViewModel _selectedSchedule;
protected override async Task OnParametersSetAsync()
{
_rowsPerPage = await _mediator.Send(new GetConfigElementByKey(ConfigElementKey.SchedulesPageSize))
.Map(maybeRows => maybeRows.Match(ce => int.TryParse(ce.Value, out int rows) ? rows : 10, () => 10));
_detailRowsPerPage = await _mediator.Send(new GetConfigElementByKey(ConfigElementKey.SchedulesDetailPageSize))
.Map(maybeRows => maybeRows.Match(ce => int.TryParse(ce.Value, out int rows) ? rows : 10, () => 10));
}
private async Task ScheduleSelected(ProgramScheduleViewModel schedule)
{
_selectedSchedule = schedule;
await _mediator.Send(new GetProgramScheduleItems(schedule.Id))
.Map(results => _selectedScheduleItems = results.OrderBy(x => x.Name).ToList());
await _detailTable.ReloadServerData();
}
private async Task DeleteSchedule(ProgramScheduleViewModel programSchedule)
@ -117,6 +123,10 @@ @@ -117,6 +123,10 @@
{
await _mediator.Send(new DeleteProgramSchedule(programSchedule.Id));
await _table.ReloadServerData();
if (_selectedSchedule == programSchedule)
{
_selectedSchedule = null;
}
}
}
@ -135,4 +145,19 @@ @@ -135,4 +145,19 @@
};
}
private async Task<TableData<ProgramScheduleItemViewModel>> DetailServerReload(TableState state)
{
await _mediator.Send(new SaveConfigElementByKey(ConfigElementKey.SchedulesDetailPageSize, state.PageSize.ToString()));
List<ProgramScheduleItemViewModel> scheduleItems = await _mediator.Send(new GetProgramScheduleItems(_selectedSchedule.Id));
IOrderedEnumerable<ProgramScheduleItemViewModel> sorted = scheduleItems.OrderBy(s => s.Index);
// TODO: properly page this data
return new TableData<ProgramScheduleItemViewModel>
{
TotalItems = scheduleItems.Count,
Items = sorted.Skip(state.Page * state.PageSize).Take(state.PageSize)
};
}
}

15
ErsatzTV/Startup.cs

@ -7,6 +7,7 @@ using Blazored.LocalStorage; @@ -7,6 +7,7 @@ using Blazored.LocalStorage;
using Dapper;
using ErsatzTV.Application;
using ErsatzTV.Application.Channels.Queries;
using ErsatzTV.Application.Logs.Queries;
using ErsatzTV.Core;
using ErsatzTV.Core.Emby;
using ErsatzTV.Core.FFmpeg;
@ -144,14 +145,21 @@ namespace ErsatzTV @@ -144,14 +145,21 @@ namespace ErsatzTV
o.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery);
o.MigrationsAssembly("ErsatzTV.Infrastructure");
}));
services.AddTransient<IDbConnection>(_ => new SqliteConnection(connectionString));
SqlMapper.AddTypeHandler(new DateTimeOffsetHandler());
SqlMapper.AddTypeHandler(new GuidHandler());
SqlMapper.AddTypeHandler(new TimeSpanHandler());
var logConnectionString = $"Data Source={FileSystemLayout.LogDatabasePath}";
services.AddDbContext<LogContext>(
options => options.UseSqlite($"Data Source={FileSystemLayout.LogDatabasePath}"));
options => options.UseSqlite(logConnectionString),
ServiceLifetime.Scoped,
ServiceLifetime.Singleton);
services.AddDbContextFactory<LogContext>(
options => options.UseSqlite(logConnectionString));
services.AddMediatR(typeof(GetAllChannels).Assembly);
@ -196,7 +204,6 @@ namespace ErsatzTV @@ -196,7 +204,6 @@ namespace ErsatzTV
services.AddScoped<IMediaItemRepository, MediaItemRepository>();
services.AddScoped<IMediaCollectionRepository, MediaCollectionRepository>();
services.AddScoped<IConfigElementRepository, ConfigElementRepository>();
services.AddScoped<ILogRepository, LogRepository>();
services.AddScoped<ITelevisionRepository, TelevisionRepository>();
services.AddScoped<ISearchRepository, SearchRepository>();
services.AddScoped<IMovieRepository, MovieRepository>();
@ -244,6 +251,8 @@ namespace ErsatzTV @@ -244,6 +251,8 @@ namespace ErsatzTV
services.AddScoped<IEmbySecretStore, EmbySecretStore>();
services.AddScoped<IEpisodeNfoReader, EpisodeNfoReader>();
// services.AddTransient(typeof(IRequestHandler<,>), typeof(GetRecentLogEntriesHandler<>));
services.AddHostedService<EndpointValidatorService>();
services.AddHostedService<DatabaseMigratorService>();
services.AddHostedService<CacheCleanerService>();

Loading…
Cancel
Save