Add details page for uploaded reports with navigation and merging functionality

This commit is contained in:
2025-09-13 04:46:11 +01:00
parent 2e02bedfb7
commit b5b6889df8
3 changed files with 702 additions and 1 deletions

View File

@@ -4,7 +4,7 @@ namespace Aaru.Server.Database.Models;
public class UploadedReportDetails public class UploadedReportDetails
{ {
public UploadedReport Report { get; set; } public UploadedReport? Report { get; set; }
public List<int> SameAll { get; set; } public List<int> SameAll { get; set; }
public List<int> SameButManufacturer { get; set; } public List<int> SameButManufacturer { get; set; }
public List<int> ReportAll { get; set; } public List<int> ReportAll { get; set; }

View File

@@ -0,0 +1,345 @@
@page "/admin/reports/{id:int}"
@attribute [Authorize]
@layout AdminLayout
@inject Microsoft.EntityFrameworkCore.IDbContextFactory<DbContext> DbContextFactory
<PageTitle>Uploaded report</PageTitle>
@if(!_initialized)
{
<div class="stats-section">
<h1 style="color: red; align-content: center; padding: 2rem">Loading...</h1>
</div>
return;
}
<section class="stats-section">
<div>
<h4>Uploaded report</h4>
<hr />
<dl class="row">
<dt class="col-sm-2">
@DisplayNameHelper.GetDisplayName(typeof(UploadedReport), nameof(UploadedReport.UploadedWhen))
</dt>
<dd class="col-sm-10">
@_model.Report?.UploadedWhen
</dd>
<dt class="col-sm-2">
@DisplayNameHelper.GetDisplayName(typeof(UploadedReport), nameof(UploadedReport.Manufacturer))
</dt>
<dd class="col-sm-10">
@_model.Report?.Manufacturer
</dd>
<dt class="col-sm-2">
@DisplayNameHelper.GetDisplayName(typeof(UploadedReport), nameof(UploadedReport.Model))
</dt>
<dd class="col-sm-10">
@_model.Report?.Model
</dd>
<dt class="col-sm-2">
@DisplayNameHelper.GetDisplayName(typeof(UploadedReport), nameof(UploadedReport.Revision))
</dt>
<dd class="col-sm-10">
@_model.Report?.Revision
</dd>
<dt class="col-sm-2">
@DisplayNameHelper.GetDisplayName(typeof(UploadedReport), nameof(UploadedReport.CompactFlash))
</dt>
<dd class="col-sm-10">
@_model.Report?.CompactFlash
</dd>
<dt class="col-sm-2">
@DisplayNameHelper.GetDisplayName(typeof(UploadedReport), nameof(UploadedReport.Type))
</dt>
<dd class="col-sm-10">
@_model.Report?.Type
</dd>
</dl>
</div>
<div>
<a href="/admin/reports/edit/@_model.Report?.Id" class="btn btn-secondary btn-sm">
<i class="bi bi-pencil"></i> Edit
</a>
<a @onclick="Promote" class="btn btn-secondary btn-sm">
<i class="bi bi-arrow-up-circle"></i> Promote
</a>
<a href="/admin/reports" class="btn btn-secondary btn-sm">
<i class="bi bi-arrow-left-circle"></i> Back to List
</a>
</div>
@if(_model.ReadCapabilitiesId != 0)
{
<div>
<a href="/admin/tested-media/@_model.ReadCapabilitiesId" target="_blank">Read capabilities</a>
</div>
}
@if(_model.Report?.ATA != null)
{
<div>
<a href="/admin/ata/@_model.Report.ATA.Id" target="_blank">ATA report</a>
</div>
}
@if(_model.Report?.ATAPI != null)
{
<div>
<a href="/admin/ata/@_model.Report.ATAPI.Id" target="_blank">ATAPI report</a>
</div>
}
@if(_model.Report?.SCSI != null)
{
<div>
<a href="/admin/scsi/@_model.Report.SCSI.Id" target="_blank">SCSI report</a>
</div>
}
@if(_model.Report?.MultiMediaCard != null)
{
<div>
<a href="/admin/mmc-sd/@_model.Report.MultiMediaCard.Id" target="_blank">MultiMediaCard report</a>
</div>
}
@if(_model.Report?.SecureDigital != null)
{
<div>
<a href="/admin/mmc-sd/@_model.Report.SecureDigital.Id" target="_blank">SecureDigital report</a>
</div>
}
@if(_model.Report?.USB != null)
{
<div>
<a href="/admin/usb/devices/@_model.Report.USB.Id" target="_blank">USB report</a>
</div>
}
@if(_model.Report?.GdRomSwapDiscCapabilitiesId != null)
{
<div>
<a href="/admin/gdrom/@_model.Report.GdRomSwapDiscCapabilitiesId" target="_blank">GD-ROM swap-trick
capabilities report</a>
</div>
}
@if(_model.Report?.FireWire != null)
{
<div>
Has a FireWire report.
</div>
}
@if(_model.Report?.PCMCIA != null)
{
<div>
Has a PCMCIA report.
</div>
}
@if(_model.SameAll.Count > 0)
{
<div>
<h4>Other uploaded reports with same manufacturer, model and revision:</h4>
<table class="table">
<thead>
<tr>
<th>
Id
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach(int item in _model.SameAll)
{
<tr>
<td>
@item
</td>
<td>
<a href="/admin/reports/@item"
class="btn btn-primary"
target="_blank">Details</a>
<a @onclick="() => Merge(_model.Report?.Id, item)"
class="btn btn-secondary">Merge</a>
</td>
</tr>
}
</tbody>
</table>
</div>
}
@if(_model.SameButManufacturer.Count > 0)
{
<div>
<h4>Other uploaded reports with same model and revision:</h4>
<table class="table">
<thead>
<tr>
<th>
Id
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach(int item in _model.SameButManufacturer)
{
<tr>
<td>
@item
</td>
<td>
<a href="/admin/reports/@item"
class="btn btn-primary"
target="_blank">Details</a>
<a @onclick="() => Merge(_model.Report?.Id, item)"
class="btn btn-secondary">Merge</a>
</td>
</tr>
}
</tbody>
</table>
</div>
}
@if(_model.ReportAll.Count > 0)
{
<div>
<h4>Device reports with same manufacturer, model and revision:</h4>
<table class="table">
<thead>
<tr>
<th>
Id
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach(int item in _model.ReportAll)
{
<tr>
<td>
@item
</td>
<td>
<a href="/admin/devices/@item"
class="btn btn-primary"
target="_blank">Details</a>
<a @onclick="() => MergeReports(item, _model.Report?.Id)"
class="btn btn-secondary">Merge</a>
</td>
</tr>
}
</tbody>
</table>
</div>
}
@if(_model.ReportButManufacturer.Count > 0)
{
<div>
<h4>Device reports with same model and revision:</h4>
<table class="table">
<thead>
<tr>
<th>
Id
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach(int item in _model.ReportButManufacturer)
{
<tr>
<td>
@item
</td>
<td>
<a href="/admin/devices/@item"
class="btn btn-primary"
target="_blank">Details</a>
<a @onclick="() => MergeReports(item, _model.Report?.Id)"
class="btn btn-secondary">Merge</a>
</td>
</tr>
}
</tbody>
</table>
</div>
}
@if(_model.TestedMedias.Count > 0)
{
<div>
<h4>Tested media:</h4>
<table class="table">
<thead>
<tr>
<th>
@DisplayNameHelper.GetDisplayName(typeof(TestedMedia), nameof(TestedMedia.Manufacturer))
</th>
<th>
@DisplayNameHelper.GetDisplayName(typeof(TestedMedia), nameof(TestedMedia.Model))
</th>
<th>
@DisplayNameHelper.GetDisplayName(typeof(TestedMedia), nameof(TestedMedia.MediumTypeName))
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach(TestedMedia item in _model.TestedMedias)
{
<tr>
<td>
@item.Manufacturer
</td>
<td>
@item.Model
</td>
<td>
@item.MediumTypeName
</td>
<td>
<a href="/admin/tested-media/@item.Id"
class="btn btn-secondary"
target="_blank">Details</a>
</td>
</tr>
}
</tbody>
</table>
</div>
}
@if(_model.TestedSequentialMedias.Count > 0)
{
<div>
<h4>Tested media:</h4>
<table class="table">
<thead>
<tr>
<th>
@DisplayNameHelper.GetDisplayName(typeof(TestedSequentialMedia), nameof(TestedSequentialMedia.Manufacturer))
</th>
<th>
@DisplayNameHelper.GetDisplayName(typeof(TestedSequentialMedia), nameof(TestedSequentialMedia.Model))
</th>
<th>
@DisplayNameHelper.GetDisplayName(typeof(TestedSequentialMedia), nameof(TestedSequentialMedia.MediumTypeName))
</th>
</tr>
</thead>
<tbody>
@foreach(TestedSequentialMedia item in _model.TestedSequentialMedias)
{
<tr>
<td>
@item.Manufacturer
</td>
<td>
@item.Model
</td>
<td>
@item.MediumTypeName
</td>
</tr>
}
</tbody>
</table>
</div>
}
</section>

View File

@@ -0,0 +1,356 @@
using Aaru.CommonTypes.Metadata;
using Aaru.Server.Database.Models;
using Microsoft.AspNetCore.Components;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using DbContext = Aaru.Server.Database.DbContext;
namespace Aaru.Server.Components.Admin.Pages.Reports;
public partial class Details
{
bool _initialized;
UploadedReportDetails _model;
[Parameter]
public int Id { get; set; }
[Inject]
NavigationManager Navigator { get; set; } = null!;
/// <inheritdoc />
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
StateHasChanged();
await using DbContext ctx = await DbContextFactory.CreateDbContextAsync();
_model = new UploadedReportDetails
{
Report = await ctx.Reports.Include(static deviceReport => deviceReport.ATAPI)
.Include(static deviceReport => deviceReport.ATA)
.ThenInclude(static ata => ata.ReadCapabilities)
.Include(static deviceReport => deviceReport.SCSI)
.ThenInclude(static scsi => scsi.ReadCapabilities)
.Include(static deviceReport => deviceReport.SCSI)
.ThenInclude(static scsi => scsi.MultiMediaDevice)
.Include(static deviceReport => deviceReport.SCSI)
.ThenInclude(static scsi => scsi.SequentialDevice)
.FirstOrDefaultAsync(m => m.Id == Id)
};
if(_model.Report is null) return;
_model.ReportAll = await ctx.Devices
.Where(d => d.Manufacturer == _model.Report.Manufacturer &&
d.Model == _model.Report.Model &&
d.Revision == _model.Report.Revision)
.Select(static d => d.Id)
.ToListAsync();
_model.ReportButManufacturer = await ctx.Devices
.Where(d => d.Model == _model.Report.Model &&
d.Revision == _model.Report.Revision)
.Select(static d => d.Id)
.Where(d => _model.ReportAll.All(r => r != d))
.ToListAsync();
_model.SameAll = await ctx.Reports
.Where(d => d.Manufacturer == _model.Report.Manufacturer &&
d.Model == _model.Report.Model &&
d.Revision == _model.Report.Revision &&
d.Id != Id)
.Select(static d => d.Id)
.ToListAsync();
_model.SameButManufacturer = await ctx.Reports
.Where(d => d.Model == _model.Report.Model &&
d.Revision == _model.Report.Revision &&
d.Id != Id)
.Select(static d => d.Id)
.Where(d => _model.SameAll.All(r => r != d))
.ToListAsync();
_model.ReadCapabilitiesId =
_model.Report.ATA?.ReadCapabilities?.Id ?? _model.Report.SCSI?.ReadCapabilities?.Id ?? 0;
// So we can check, as we know IDs with 0 will never exist, and EFCore does not allow null propagation in the LINQ
int ataId = _model.Report.ATA?.Id ?? 0;
int atapiId = _model.Report.ATAPI?.Id ?? 0;
int scsiId = _model.Report.SCSI?.Id ?? 0;
int mmcId = _model.Report.SCSI?.MultiMediaDevice?.Id ?? 0;
int sscId = _model.Report.SCSI?.SequentialDevice?.Id ?? 0;
_model.TestedMedias = await ctx.TestedMedia
.Where(t => t.AtaId == ataId ||
t.AtaId == atapiId ||
t.ScsiId == scsiId ||
t.MmcId == mmcId)
.OrderBy(static t => t.Manufacturer)
.ThenBy(static t => t.Model)
.ThenBy(static t => t.MediumTypeName)
.ToListAsync();
_model.TestedSequentialMedias = await ctx.TestedSequentialMedia.Where(t => t.SscId == sscId)
.OrderBy(static t => t.Manufacturer)
.ThenBy(static t => t.Model)
.ThenBy(static t => t.MediumTypeName)
.ToListAsync();
_initialized = true;
StateHasChanged();
}
async Task Promote()
{
await using DbContext ctx = await DbContextFactory.CreateDbContextAsync();
UploadedReport? uploadedReport = await ctx.Reports.FirstOrDefaultAsync(m => m.Id == Id);
if(uploadedReport == null) return;
var device = new Device(uploadedReport.ATAId,
uploadedReport.ATAPIId,
uploadedReport.FireWireId,
uploadedReport.MultiMediaCardId,
uploadedReport.PCMCIAId,
uploadedReport.SecureDigitalId,
uploadedReport.SCSIId,
uploadedReport.USBId,
uploadedReport.UploadedWhen,
uploadedReport.Manufacturer,
uploadedReport.Model,
uploadedReport.Revision,
uploadedReport.CompactFlash,
uploadedReport.Type,
uploadedReport.GdRomSwapDiscCapabilitiesId);
EntityEntry<Device> res = ctx.Devices.Add(device);
ctx.Reports.Remove(uploadedReport);
await ctx.SaveChangesAsync();
Navigator.NavigateTo($"/admin/devices/{res.Entity.Id}");
}
async Task Merge(int? master, int? slave)
{
if(master is null || slave is null) return;
await using DbContext ctx = await DbContextFactory.CreateDbContextAsync();
UploadedReport? masterReport = await ctx.Reports.Include(static deviceReport => deviceReport.SCSI)
.FirstOrDefaultAsync(m => m.Id == master);
UploadedReport? slaveReport = await ctx.Reports.Include(static deviceReport => deviceReport.SCSI)
.FirstOrDefaultAsync(m => m.Id == slave);
if(masterReport is null || slaveReport is null) return;
if(masterReport.ATAId != null && masterReport.ATAId != slaveReport.ATAId)
{
foreach(CommonTypes.Metadata.TestedMedia testedMedia in
ctx.TestedMedia.Where(d => d.AtaId == slaveReport.ATAId))
{
testedMedia.AtaId = masterReport.ATAId;
ctx.Update(testedMedia);
}
}
else if(masterReport.ATAId == null && slaveReport.ATAId != null)
{
masterReport.ATAId = slaveReport.ATAId;
ctx.Update(masterReport);
}
if(masterReport.ATAPIId != null && masterReport.ATAPIId != slaveReport.ATAPIId)
{
foreach(CommonTypes.Metadata.TestedMedia testedMedia in
ctx.TestedMedia.Where(d => d.AtaId == slaveReport.ATAPIId))
{
testedMedia.AtaId = masterReport.ATAPIId;
ctx.Update(testedMedia);
}
}
else if(masterReport.ATAPIId == null && slaveReport.ATAPIId != null)
{
masterReport.ATAPIId = slaveReport.ATAPIId;
ctx.Update(masterReport);
}
if(masterReport.SCSIId != null && masterReport.SCSIId != slaveReport.SCSIId)
{
foreach(CommonTypes.Metadata.TestedMedia testedMedia in
ctx.TestedMedia.Where(d => d.ScsiId == slaveReport.SCSIId))
{
testedMedia.ScsiId = masterReport.SCSIId;
ctx.Update(testedMedia);
}
}
else if(masterReport.SCSIId == null && slaveReport.SCSIId != null)
{
masterReport.SCSIId = slaveReport.SCSIId;
ctx.Update(masterReport);
}
if(masterReport.SCSI?.SequentialDeviceId != null &&
masterReport.SCSI?.SequentialDeviceId != slaveReport.SCSI?.SequentialDeviceId)
{
foreach(TestedSequentialMedia testedMedia in
ctx.TestedSequentialMedia.Where(d => slaveReport.SCSI != null &&
d.SscId == slaveReport.SCSI.SequentialDeviceId))
{
if(masterReport.SCSI != null) testedMedia.SscId = masterReport.SCSI.SequentialDeviceId;
ctx.Update(testedMedia);
}
}
else if(masterReport.SCSI is { SequentialDeviceId: null } && slaveReport.SCSI?.SequentialDeviceId != null)
{
if(masterReport.SCSI != null) masterReport.SCSI.SequentialDeviceId = slaveReport.SCSI.SequentialDeviceId;
ctx.Update(masterReport);
}
if(masterReport.GdRomSwapDiscCapabilitiesId == null && slaveReport.GdRomSwapDiscCapabilitiesId != null ||
masterReport.GdRomSwapDiscCapabilitiesId != null && slaveReport.GdRomSwapDiscCapabilitiesId != null)
{
masterReport.GdRomSwapDiscCapabilitiesId = slaveReport.GdRomSwapDiscCapabilitiesId;
ctx.Update(masterReport);
}
ctx.Remove(slaveReport);
await ctx.SaveChangesAsync();
Navigator.Refresh(true);
}
async Task MergeReports(int? deviceId, int? reportId)
{
if(deviceId is null || reportId is null) return;
await using DbContext ctx = await DbContextFactory.CreateDbContextAsync();
Device? device = await ctx.Devices.Include(static deviceReport => deviceReport.ATA)
.ThenInclude(static ata => ata.ReadCapabilities)
.Include(static deviceReport => deviceReport.SCSI)
.ThenInclude(static scsi => scsi.ReadCapabilities)
.Include(static deviceReport => deviceReport.SCSI)
.ThenInclude(static scsi => scsi.MultiMediaDevice)
.Include(static deviceReport => deviceReport.SCSI)
.ThenInclude(static scsi => scsi.SequentialDevice)
.FirstOrDefaultAsync(m => m.Id == deviceId);
UploadedReport? report = await ctx.Reports.Include(static deviceReport => deviceReport.ATA)
.ThenInclude(static ata => ata.ReadCapabilities)
.Include(static deviceReport => deviceReport.SCSI)
.ThenInclude(static scsi => scsi.ReadCapabilities)
.Include(static deviceReport => deviceReport.SCSI)
.ThenInclude(static scsi => scsi.MultiMediaDevice)
.Include(static deviceReport => deviceReport.SCSI)
.ThenInclude(static scsi => scsi.SequentialDevice)
.FirstOrDefaultAsync(m => m.Id == reportId);
if(device?.ATAId != null && device.ATAId != report?.ATAId)
{
foreach(CommonTypes.Metadata.TestedMedia testedMedia in
ctx.TestedMedia.Where(d => report != null && d.AtaId == report.ATAId))
{
testedMedia.AtaId = device.ATAId;
ctx.Update(testedMedia);
}
if(device.ATA is { ReadCapabilities: null } && report?.ATA?.ReadCapabilities != null)
{
device.ATA.ReadCapabilities = report.ATA.ReadCapabilities;
ctx.Update(device.ATA);
}
}
else if(device?.ATAId == null && report?.ATAId != null)
{
if(device != null)
{
device.ATAId = report.ATAId;
ctx.Update(device);
}
}
switch(device)
{
case { ATAPIId: not null } when device.ATAPIId != report?.ATAPIId:
{
foreach(CommonTypes.Metadata.TestedMedia testedMedia in
ctx.TestedMedia.Where(d => report != null && d.AtaId == report.ATAPIId))
{
testedMedia.AtaId = device.ATAPIId;
ctx.Update(testedMedia);
}
break;
}
case { ATAPIId: null } when report?.ATAPIId != null:
device.ATAPIId = report.ATAPIId;
ctx.Update(device);
break;
}
switch(device)
{
case { SCSIId: not null } when device.SCSIId != report?.SCSIId:
{
foreach(CommonTypes.Metadata.TestedMedia testedMedia in ctx.TestedMedia.Where(d => report != null &&
d.ScsiId == report.SCSIId))
{
testedMedia.ScsiId = device.SCSIId;
ctx.Update(testedMedia);
}
if(device.SCSI is { ReadCapabilities: null } && report?.SCSI?.ReadCapabilities != null)
{
device.SCSI.ReadCapabilities = report.SCSI.ReadCapabilities;
ctx.Update(device.SCSI);
}
if(device.SCSI is { MultiMediaDevice: null } && report?.SCSI?.MultiMediaDevice != null)
{
device.SCSI.MultiMediaDevice = report.SCSI.MultiMediaDevice;
ctx.Update(device.SCSI);
}
else if(device.SCSI?.MultiMediaDevice != null && report?.SCSI?.MultiMediaDevice != null)
{
foreach(CommonTypes.Metadata.TestedMedia testedMedia in ctx.TestedMedia.Where(d => d.MmcId ==
report.SCSI.MultiMediaDevice.Id))
{
testedMedia.MmcId = device.SCSI.MultiMediaDevice.Id;
ctx.Update(testedMedia);
}
}
if(device.SCSI is { SequentialDevice: null } && report?.SCSI?.SequentialDevice != null)
{
device.SCSI.SequentialDevice = report.SCSI.SequentialDevice;
ctx.Update(device.SCSI);
}
else if(device.SCSI?.SequentialDevice != null && report?.SCSI?.SequentialDevice != null)
{
foreach(TestedSequentialMedia testedSequentialMedia in
ctx.TestedSequentialMedia.Where(d => d.SscId == report.SCSI.SequentialDevice.Id))
{
testedSequentialMedia.SscId = device.SCSI.SequentialDevice.Id;
ctx.Update(testedSequentialMedia);
}
}
break;
}
case { SCSIId: null } when report?.SCSIId != null:
device.SCSIId = report.SCSIId;
ctx.Update(device);
break;
}
ctx.Remove(report);
await ctx.SaveChangesAsync();
Navigator.Refresh(true);
}
}