Add Upload Report functionality and navigation

This commit is contained in:
2026-01-02 15:56:54 +00:00
parent a7f6df30e2
commit 70dd7f7142
5 changed files with 238 additions and 2 deletions

View File

@@ -263,6 +263,10 @@
<span class="bi bi-file-earmark-text-nav-menu"></span>
<span class="nav-link-text">Uploaded Reports</span>
</NavLink>
<NavLink class="nav-link" href="/admin/reports/upload" style="@GetLinkVisibility("upload report")">
<span class="bi bi-cloud-arrow-up-nav-menu"></span>
<span class="nav-link-text">Upload Report</span>
</NavLink>
</div>
</div>
@@ -744,9 +748,14 @@
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' viewBox='0 0 16 16'%3E%3Cpath d='M5.5 7a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1h-5zM5 9.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5zm0 2a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5z'/%3E%3Cpath d='M9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4.5L9.5 0zm0 1v2A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5z'/%3E%3C/svg%3E");
}
.bi-chevron-down-nav-menu
.bi-cloud-arrow-up-nav-menu
{
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3E%3C/svg%3E");
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M7.646 5.146a.5.5 0 0 1 .708 0l2 2a.5.5 0 0 1-.708.708L8.5 6.707V10.5a.5.5 0 0 1-1 0V6.707L6.354 7.854a.5.5 0 1 1-.708-.708l2-2z'/%3E%3Cpath d='M4.406 3.504a.75.75 0 0 1 .992.06l2.286 2.393.992-1.391a.75.75 0 1 1 1.23.888l-1.868 2.612a.75.75 0 0 1-1.06.06L4.338 4.496a.75.75 0 0 1 .068-1.061z'/%3E%3Cpath d='M11.5 6h.5a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-7a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h.5a.75.75 0 0 0 0-1.5h-.5A3.5 3.5 0 0 0 1.5 13v5a3.5 3.5 0 0 0 3.5 3.5h7a3.5 3.5 0 0 0 3.5-3.5v-5a3.5 3.5 0 0 0-3.5-3.5z'/%3E%3C/svg%3E");
}
{
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3E%3C/svg%3E")
;
}
.bi-chevron-right-nav-menu

View File

@@ -59,6 +59,9 @@
</h1>
<p class="text-secondary mb-0">Manage device identification reports</p>
</div>
<a href="/admin/reports/upload" class="btn btn-success">
<i class="bi bi-cloud-arrow-up"></i> Upload Report
</a>
</div>
<!-- Search and Filter Bar -->

View File

@@ -0,0 +1,82 @@
@page "/admin/reports/upload"
@attribute [Authorize]
@layout AdminLayout
@rendermode InteractiveServer
<PageTitle>Upload Device Report</PageTitle>
<div class="container-fluid px-4 py-4">
<!-- Header Section -->
<div class="mb-4">
<h1 class="h2 mb-1 text-light">
<i class="bi bi-cloud-arrow-up-nav-menu"></i> Upload Device Report
</h1>
<p class="text-secondary mb-0">Upload a device identification report in JSON format</p>
</div>
<div class="row">
<div class="col-12">
<div class="card bg-dark shadow-sm border-secondary">
<div class="card-header bg-info text-dark">
<h5 class="mb-0">Select and Upload Report</h5>
</div>
<div class="card-body">
@if(!string.IsNullOrEmpty(_errorMessage))
{
<div class="alert alert-danger alert-dismissible fade show" role="alert">
@_errorMessage
<button type="button" class="btn-close" @onclick="@(() => _errorMessage = null)"></button>
</div>
}
@if(!string.IsNullOrEmpty(_successMessage))
{
<div class="alert alert-success alert-dismissible fade show" role="alert">
@_successMessage
<button type="button" class="btn-close" @onclick="@(() => _successMessage = null)"></button>
</div>
}
<div class="mb-4">
<label for="jsonFile" class="form-label text-light">Select JSON Report File</label>
<InputFile class="form-control bg-dark text-light border-secondary" id="jsonFile" accept=".json"
@ref="_fileInput" OnChange="HandleFileSelected" />
<small class="form-text text-secondary">
Select a valid device report JSON file (.json)
</small>
</div>
@if(_fileSelected)
{
<div class="mb-4">
<h5 class="text-light">File Information</h5>
<div class="alert alert-secondary text-dark">
<strong>File:</strong> @_selectedFileName<br />
<strong>Size:</strong> @FormatFileSize(_selectedFileSize)<br />
</div>
</div>
}
<div class="d-flex gap-2">
<button class="btn btn-success" @onclick="UploadFile" disabled="@_isUploading">
@if(_isUploading)
{
<span class="spinner-border spinner-border-sm me-2"
role="status"
aria-hidden="true"></span>
<span>Uploading...</span>
}
else
{
<i class="bi bi-cloud-arrow-up"></i>
<span>Upload Report</span>
}
</button>
<a class="btn btn-secondary" href="/admin/reports">Cancel</a>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,140 @@
using System.Text;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
namespace Aaru.Server.Components.Admin.Pages.Reports;
public partial class Upload : ComponentBase
{
private string _errorMessage;
private byte[] _fileContent;
private InputFile _fileInput;
private bool _fileSelected;
private bool _isUploading;
private string _selectedFileName;
private long _selectedFileSize;
private string _successMessage;
[Inject]
private HttpClient Http { get; set; }
[Inject]
private NavigationManager NavigationManager { get; set; }
private async Task HandleFileSelected(InputFileChangeEventArgs e)
{
_errorMessage = null;
_successMessage = null;
_fileSelected = false;
try
{
IBrowserFile? file = e.File;
if(file == null)
{
_errorMessage = "No file selected.";
return;
}
if(!file.Name.EndsWith(".json", StringComparison.OrdinalIgnoreCase))
{
_errorMessage = "Please select a valid JSON file (.json)";
return;
}
const long maxFileSize = 10 * 1024 * 1024; // 10 MB
if(file.Size > maxFileSize)
{
_errorMessage = $"File size exceeds the maximum allowed size of {FormatFileSize(maxFileSize)}";
return;
}
_selectedFileName = file.Name;
_selectedFileSize = file.Size;
using var memoryStream = new MemoryStream();
await file.OpenReadStream(maxFileSize).CopyToAsync(memoryStream);
_fileContent = memoryStream.ToArray();
_fileSelected = true;
}
catch(Exception ex)
{
_errorMessage = $"Error reading file: {ex.Message}";
}
}
private async Task UploadFile()
{
if(!_fileSelected)
{
_errorMessage = "Please select a file first.";
return;
}
_isUploading = true;
_errorMessage = null;
_successMessage = null;
try
{
string jsonContent = Encoding.UTF8.GetString(_fileContent);
using var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
var uploadUrl = NavigationManager.ToAbsoluteUri("/api/uploadreportv2").ToString();
using HttpResponseMessage response = await Http.PostAsync(uploadUrl, content);
string? responseContent = await response.Content.ReadAsStringAsync();
if(response.IsSuccessStatusCode && responseContent == "ok")
{
_successMessage = "Report uploaded successfully!";
_fileSelected = false;
_selectedFileName = null;
_selectedFileSize = 0;
_fileContent = null;
// Reset file input
if(_fileInput != null)
{
// Navigate away after a short delay to show success message
await Task.Delay(1500);
NavigationManager.NavigateTo("/admin/reports");
}
}
else if(responseContent == "notstats")
_errorMessage = "Invalid report format: The uploaded file is not a valid device report.";
else
_errorMessage = $"Upload failed: {responseContent ?? response.ReasonPhrase}";
}
catch(Exception ex)
{
_errorMessage = $"Error uploading file: {ex.Message}";
}
finally
{
_isUploading = false;
}
}
private string FormatFileSize(long bytes)
{
string[] sizes = ["B", "KB", "MB", "GB"];
double len = bytes;
var order = 0;
while(len >= 1024 && order < sizes.Length - 1)
{
order++;
len = len / 1024;
}
return $"{len:0.##} {sizes[order]}";
}
}

View File

@@ -165,6 +165,8 @@ builder.WebHost.UseSentry(o =>
// Add services to the container.
builder.Services.AddRazorComponents().AddInteractiveServerComponents();
builder.Services.AddScoped<HttpClient>();
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddScoped<IdentityUserAccessor>();
builder.Services.AddScoped<IdentityRedirectManager>();