Compare commits
2 Commits
f6a69d1673
...
f6f8a3bf65
| Author | SHA1 | Date | |
|---|---|---|---|
| f6f8a3bf65 | |||
|
|
a8296063f5 |
|
|
@ -59,7 +59,7 @@
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@code{
|
@code {
|
||||||
public class NavModel
|
public class NavModel
|
||||||
{
|
{
|
||||||
public string Icon { get; set; } = "";
|
public string Icon { get; set; } = "";
|
||||||
|
|
@ -72,8 +72,9 @@
|
||||||
new(){Icon = "mdi-view-dashboard", Path="/", Label = "Dashboard", Match = NavLinkMatch.All},
|
new(){Icon = "mdi-view-dashboard", Path="/", Label = "Dashboard", Match = NavLinkMatch.All},
|
||||||
// new(){Icon = "mdi-map-legend", Path="/maps-manager", Label = "Mapping", Match = NavLinkMatch.All},
|
// new(){Icon = "mdi-map-legend", Path="/maps-manager", Label = "Mapping", Match = NavLinkMatch.All},
|
||||||
new(){Icon = "mdi-monitor", Path="/robot-monitor", Label = "Robot Monitor", Match = NavLinkMatch.All},
|
new(){Icon = "mdi-monitor", Path="/robot-monitor", Label = "Robot Monitor", Match = NavLinkMatch.All},
|
||||||
new(){Icon = "mdi-application-cog", Path="/robot-config", Label = "Config", Match = NavLinkMatch.All},
|
|
||||||
new(){Icon = "mdi-state-machine", Path="/robot-order", Label = "order", Match = NavLinkMatch.All},
|
new(){Icon = "mdi-state-machine", Path="/robot-order", Label = "order", Match = NavLinkMatch.All},
|
||||||
|
new(){Icon = "mdi-application-cog", Path="/robot-config", Label = "Config", Match = NavLinkMatch.All },
|
||||||
|
new(){Icon = "mdi-math-log", Path="/logs", Label = "Logs", Match = NavLinkMatch.All},
|
||||||
];
|
];
|
||||||
|
|
||||||
private bool collapseNavMenu = true;
|
private bool collapseNavMenu = true;
|
||||||
|
|
|
||||||
40
RobotApp.Client/Models/LoggerModel.cs
Normal file
40
RobotApp.Client/Models/LoggerModel.cs
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace RobotApp.Client.Models;
|
||||||
|
|
||||||
|
public class LoggerModel
|
||||||
|
{
|
||||||
|
[JsonPropertyName("time")]
|
||||||
|
public string? Time { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("level")]
|
||||||
|
public string? Level { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("message")]
|
||||||
|
public string? Message { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("exception")]
|
||||||
|
public string? Exception { get; set; }
|
||||||
|
|
||||||
|
public string ColorClass => Level switch
|
||||||
|
{
|
||||||
|
"WARN" => "text-warning",
|
||||||
|
"INFO" => "text-info",
|
||||||
|
"DEBUG" => "text-success",
|
||||||
|
"ERROR" => "text-danger",
|
||||||
|
"FATAL" => "text-secondary",
|
||||||
|
_ => "text-muted",
|
||||||
|
};
|
||||||
|
|
||||||
|
public string BackgroundClass => Level switch
|
||||||
|
{
|
||||||
|
"WARN" => "bg-warning text-dark",
|
||||||
|
"INFO" => "bg-info text-dark",
|
||||||
|
"DEBUG" => "bg-success text-white",
|
||||||
|
"ERROR" => "bg-danger text-white",
|
||||||
|
"FATAL" => "bg-secondary text-white",
|
||||||
|
_ => "bg-dark text-white",
|
||||||
|
};
|
||||||
|
|
||||||
|
public bool HasException => !string.IsNullOrEmpty(Exception);
|
||||||
|
}
|
||||||
176
RobotApp.Client/Pages/Logs.razor
Normal file
176
RobotApp.Client/Pages/Logs.razor
Normal file
|
|
@ -0,0 +1,176 @@
|
||||||
|
@page "/logs"
|
||||||
|
@rendermode InteractiveWebAssemblyNoPrerender
|
||||||
|
@attribute [Authorize]
|
||||||
|
|
||||||
|
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
|
@using RobotApp.Client.Models
|
||||||
|
|
||||||
|
@inject IJSRuntime JSRuntime
|
||||||
|
@inject HttpClient Http
|
||||||
|
@inject IConfiguration Configuration
|
||||||
|
@inject ISnackbar Snackbar
|
||||||
|
|
||||||
|
<PageTitle>Logs</PageTitle>
|
||||||
|
|
||||||
|
<div class="w-100 h-100 d-flex flex-column">
|
||||||
|
<div class="d-flex flex-row align-items-center justify-content-between" style="border-bottom: 1px solid silver">
|
||||||
|
<MudTextField Class="mt-1 ms-2" T="string" Value="FilterLog" Adornment="Adornment.End" ValueChanged="OnSearch" AdornmentIcon="@Icons.Material.Filled.Search"
|
||||||
|
IconSize="Size.Medium" Variant="Variant.Outlined" Margin="Margin.Dense" AdornmentColor="Color.Secondary" Label="Search"></MudTextField>
|
||||||
|
<MudSpacer />
|
||||||
|
<div class="m-1 d-flex flex-row">
|
||||||
|
<MudDatePicker Class="mx-4" Label="Date" Date="DateLog" DateChanged="OnDateChanged" MaxDate="DateTime.Today" Variant="Variant.Outlined" Color="Color.Primary"
|
||||||
|
ShowToolbar="false" Margin="Margin.Dense" AdornmentColor="Color.Primary" />
|
||||||
|
|
||||||
|
<MudTooltip Text="Export">
|
||||||
|
<MudFab Class="mt-2" Color="Color.Info" StartIcon="@Icons.Material.Filled.ImportExport" Size="Size.Small" OnClick="ExportLogs" />
|
||||||
|
</MudTooltip>
|
||||||
|
<MudTooltip Text="Refresh">
|
||||||
|
<MudFab Class="mx-4 mt-2" StartIcon="@Icons.Material.Filled.Refresh" Color="Color.Primary" Size="Size.Small" OnClick="LoadLogs" />
|
||||||
|
</MudTooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1 mt-2 ms-2 position-relative" style="background-color: rgba(0, 0, 0, 0);">
|
||||||
|
<MudOverlay Visible="IsLoading" DarkBackground="true" Absolute="true">
|
||||||
|
<MudProgressCircular Color="Color.Info" Indeterminate="true" />
|
||||||
|
</MudOverlay>
|
||||||
|
<div class="h-100 w-100 position-relative">
|
||||||
|
<div class="log-container" @ref="LogContainerRef">
|
||||||
|
@if (ShowRawLog)
|
||||||
|
{
|
||||||
|
<div class="d-flex justify-content-center my-3">
|
||||||
|
<div><MudButton Variant="Variant.Outlined" Size="Size.Small" OnClick="@(() => ShowRawLog = false)">Normal</MudButton></div>
|
||||||
|
</div>
|
||||||
|
<big style="font-size: 14px;">
|
||||||
|
@foreach (var log in ShowLogs)
|
||||||
|
{
|
||||||
|
@log <br />
|
||||||
|
}
|
||||||
|
</big>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
@if (SearchLogs.Count < ShowLogs.Count)
|
||||||
|
{
|
||||||
|
<div class="d-flex justify-content-center my-3">
|
||||||
|
<div><MudButton Variant="Variant.Outlined" Size="Size.Small" OnClick="@(() => ShowRawLog = true)">Raw log</MudButton></div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@foreach (var log in SearchLogs)
|
||||||
|
{
|
||||||
|
<div class="log">
|
||||||
|
<span class="log-head @log.BackgroundClass">
|
||||||
|
@log.Time <span class="log-level">@log.Level</span>
|
||||||
|
</span>
|
||||||
|
<span>@log.Message</span>
|
||||||
|
@if (log.HasException)
|
||||||
|
{
|
||||||
|
<br />
|
||||||
|
<pre class="log-exception">
|
||||||
|
@log.Exception
|
||||||
|
</pre>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private DateTime DateLog = DateTime.Today;
|
||||||
|
private bool IsLoading;
|
||||||
|
private readonly List<string> ShowLogs = new();
|
||||||
|
private readonly List<LoggerModel> SearchLogs = new();
|
||||||
|
private ElementReference LogContainerRef { get; set; }
|
||||||
|
private bool ShowRawLog { get; set; }
|
||||||
|
private string? FilterLog { get; set; }
|
||||||
|
|
||||||
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
|
{
|
||||||
|
await base.OnAfterRenderAsync(firstRender);
|
||||||
|
if (!firstRender) return;
|
||||||
|
|
||||||
|
await LoadLogs();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadLogs()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
IsLoading = true;
|
||||||
|
ShowLogs.Clear();
|
||||||
|
StateHasChanged();
|
||||||
|
|
||||||
|
var logs = await Http.GetFromJsonAsync<IEnumerable<string>>($"api/LogsManager?date={DateLog}");
|
||||||
|
ShowLogs.AddRange(logs ?? []);
|
||||||
|
|
||||||
|
IsLoading = false;
|
||||||
|
StateHasChanged();
|
||||||
|
|
||||||
|
await ReloadLogs();
|
||||||
|
}
|
||||||
|
catch (AccessTokenNotAvailableException ex)
|
||||||
|
{
|
||||||
|
ex.Redirect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ReloadLogs()
|
||||||
|
{
|
||||||
|
IsLoading = true;
|
||||||
|
SearchLogs.Clear();
|
||||||
|
StateHasChanged();
|
||||||
|
|
||||||
|
foreach (var line in ShowLogs.Where(log => string.IsNullOrEmpty(FilterLog) || log.Contains(FilterLog)).TakeLast(2000))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var log = System.Text.Json.JsonSerializer.Deserialize<LoggerModel>(line);
|
||||||
|
if (log is not null) SearchLogs.Add(log);
|
||||||
|
}
|
||||||
|
catch (System.Text.Json.JsonException)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IsLoading = false;
|
||||||
|
StateHasChanged();
|
||||||
|
await JSRuntime.InvokeVoidAsync("ScrollToBottom", LogContainerRef);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnSearch(string text)
|
||||||
|
{
|
||||||
|
FilterLog = text;
|
||||||
|
await ReloadLogs();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnDateChanged(DateTime? date)
|
||||||
|
{
|
||||||
|
if (date is not null && date.HasValue)
|
||||||
|
{
|
||||||
|
DateLog = date.Value;
|
||||||
|
await LoadLogs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ExportLogs()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var fileContent = await Http.GetFromJsonAsync<IEnumerable<string>>($"api/LogsManager?date={DateLog}");
|
||||||
|
var formattedContent = string.Join("\n", fileContent ?? []);
|
||||||
|
var fileName = $"LogsManager_{DateLog.ToShortDateString()}.txt";
|
||||||
|
await JSRuntime.InvokeVoidAsync("downloadFile", fileName, formattedContent, "text/plain");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Snackbar.Add($"Lỗi khi tải file: {ex.Message}", Severity.Warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
38
RobotApp.Client/Pages/Logs.razor.css
Normal file
38
RobotApp.Client/Pages/Logs.razor.css
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
.log-container {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
position: absolute;
|
||||||
|
top: 0px;
|
||||||
|
left: 0px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log {
|
||||||
|
word-wrap: break-word;
|
||||||
|
line-height: 18px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-logger {
|
||||||
|
color: rgba(0, 0, 0, 0.3);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-level {
|
||||||
|
display: inline-block;
|
||||||
|
width: 46px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-head {
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 2px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-exception {
|
||||||
|
line-height: 16px;
|
||||||
|
margin-left: 30px;
|
||||||
|
color: crimson;
|
||||||
|
}
|
||||||
|
|
@ -22,8 +22,4 @@
|
||||||
<ProjectReference Include="..\RobotApp.VDA5050\RobotApp.VDA5050.csproj" />
|
<ProjectReference Include="..\RobotApp.VDA5050\RobotApp.VDA5050.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Folder Include="Models\" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
48
RobotApp/Controllers/LogsManagerController.cs
Normal file
48
RobotApp/Controllers/LogsManagerController.cs
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace RobotApp.Controllers;
|
||||||
|
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[ApiController]
|
||||||
|
[Authorize]
|
||||||
|
public class LogsManagerController(Services.Logger<LogsManagerController> Logger) : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly string LoggerDirectory = "logs";
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IEnumerable<string>> GetLogs([FromQuery(Name = "date")] DateTime date)
|
||||||
|
{
|
||||||
|
string temp = "";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string fileName = $"{date:yyyy-MM-dd}.log";
|
||||||
|
string path = Path.Combine(LoggerDirectory, fileName);
|
||||||
|
if (!Path.GetFullPath(path).StartsWith(Path.GetFullPath(LoggerDirectory)))
|
||||||
|
{
|
||||||
|
Logger.Warning($"GetLogs: phát hiện đường dẫn không hợp lệ.");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!System.IO.File.Exists(path))
|
||||||
|
{
|
||||||
|
Logger.Warning($"GetLogs: không tìm thấy file log của ngày {date.ToShortDateString()} - {path}.");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
temp = Path.Combine(LoggerDirectory, $"{Guid.NewGuid()}.log");
|
||||||
|
System.IO.File.Copy(path, temp);
|
||||||
|
|
||||||
|
return await System.IO.File.ReadAllLinesAsync(temp);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.Warning($"GetLogs: Hệ thống có lỗi xảy ra - {ex.Message}");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (System.IO.File.Exists(temp)) System.IO.File.Delete(temp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
"dotnetRunMessages": true,
|
"dotnetRunMessages": true,
|
||||||
"launchBrowser": true,
|
"launchBrowser": true,
|
||||||
"workingDirectory": "$(TargetDir)",
|
"workingDirectory": "$(TargetDir)",
|
||||||
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
//"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||||
"applicationUrl": "https://0.0.0.0:7150;http://localhost:5229",
|
"applicationUrl": "https://0.0.0.0:7150;http://localhost:5229",
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,8 @@ public class MQTTClient : IAsyncDisposable
|
||||||
public event Action<string>? InstanceActionsChanged;
|
public event Action<string>? InstanceActionsChanged;
|
||||||
public bool IsConnected => !IsDisposed && MqttClient is not null && MqttClient.IsConnected;
|
public bool IsConnected => !IsDisposed && MqttClient is not null && MqttClient.IsConnected;
|
||||||
|
|
||||||
private string OrderTopic => $"{VDA5050Setting.TopicPrefix}/{ClientId}/{VDA5050Topic.ORDER.ToTopicString()}";
|
private string OrderTopic => $"{VDA5050Setting.TopicPrefix}/{VDA5050Setting.Manufacturer}/{ClientId}/{VDA5050Topic.ORDER.ToTopicString()}";
|
||||||
private string InstanceActionsTopic => $"{VDA5050Setting.TopicPrefix}/{ClientId}/{VDA5050Topic.INSTANTACTIONS.ToTopicString()}";
|
private string InstanceActionsTopic => $"{VDA5050Setting.TopicPrefix}/{VDA5050Setting.Manufacturer}/{ClientId}/{VDA5050Topic.INSTANTACTIONS.ToTopicString()}";
|
||||||
|
|
||||||
public MQTTClient(string clientId, VDA5050Setting setting, Logger<MQTTClient> logger)
|
public MQTTClient(string clientId, VDA5050Setting setting, Logger<MQTTClient> logger)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ public class RobotConnection(RobotConfiguration RobotConfiguration,
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Logger.Debug($"Nhận Order: {data}");
|
//Logger.Debug($"Nhận Order: {data}");
|
||||||
var msg = JsonSerializer.Deserialize<OrderMsg>(data, JsonOptionExtends.Read);
|
var msg = JsonSerializer.Deserialize<OrderMsg>(data, JsonOptionExtends.Read);
|
||||||
if (msg is null || string.IsNullOrEmpty(msg.SerialNumber) || msg.SerialNumber != RobotConfiguration.SerialNumber) return;
|
if (msg is null || string.IsNullOrEmpty(msg.SerialNumber) || msg.SerialNumber != RobotConfiguration.SerialNumber) return;
|
||||||
OrderUpdated?.Invoke(msg);
|
OrderUpdated?.Invoke(msg);
|
||||||
|
|
@ -37,7 +37,7 @@ public class RobotConnection(RobotConfiguration RobotConfiguration,
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Logger.Debug($"Nhận InstanceActions: {data}");
|
//Logger.Debug($"Nhận InstanceActions: {data}");
|
||||||
var msg = JsonSerializer.Deserialize<InstantActionsMsg>(data, JsonOptionExtends.Read);
|
var msg = JsonSerializer.Deserialize<InstantActionsMsg>(data, JsonOptionExtends.Read);
|
||||||
if (msg is null || string.IsNullOrEmpty(msg.SerialNumber) || msg.SerialNumber != RobotConfiguration.SerialNumber) return;
|
if (msg is null || string.IsNullOrEmpty(msg.SerialNumber) || msg.SerialNumber != RobotConfiguration.SerialNumber) return;
|
||||||
ActionUpdated?.Invoke(msg);
|
ActionUpdated?.Invoke(msg);
|
||||||
|
|
@ -50,7 +50,7 @@ public class RobotConnection(RobotConfiguration RobotConfiguration,
|
||||||
|
|
||||||
public async Task<MessageResult> Publish(string topic, string data)
|
public async Task<MessageResult> Publish(string topic, string data)
|
||||||
{
|
{
|
||||||
if (MqttClient is not null && MqttClient.IsConnected) return await MqttClient.PublishAsync($"{VDA5050Setting.TopicPrefix}/{RobotConfiguration.SerialNumber}/{topic}", data);
|
if (MqttClient is not null && MqttClient.IsConnected) return await MqttClient.PublishAsync($"{VDA5050Setting.TopicPrefix}/{VDA5050Setting.Manufacturer}/{RobotConfiguration.SerialNumber}/{topic}", data);
|
||||||
return new(false, "Chưa có kết nối tới broker");
|
return new(false, "Chưa có kết nối tới broker");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user