RobotNet/RobotNet.WebApp/Pages/Missions.razor
2025-10-15 15:15:53 +07:00

304 lines
12 KiB
Plaintext

@page "/missions"
@attribute [Authorize]
@using RobotNet.Script.Shares
@using RobotNet.Shares
@using RobotNet.Clients
@using RobotNet.WebApp.Scripts.Models
@inject IHttpClientFactory httpFactory
@inject IDialogService DialogService
@inject ISnackbar Snackbar
<PageTitle>Runner Missions</PageTitle>
<div @ref="divRef" class="w-100 h-100">
<MudTable @ref="table" ServerData="ServerReload" Style="margin: 10px;" Items="runnerMissions" Height="@TableHeight" Dense FixedHeader Virtualize Hover Elevation="6">
<ToolBarContent>
<div @ref="toolbarRef" class="w-100 d-flex flex-row">
<h3>Danh sách Missions</h3>
<MudSpacer />
<MudIconButton Class="me-2" Icon="@Icons.Material.Filled.Refresh" Color="Color.Primary" Size="Size.Small" OnClick="ReloadTable" />
<MudTextField T="string" ValueChanged="@(s => OnSearch(s))" DebounceInterval="1000" Value="@searchString" Margin="Margin.Dense"
Placeholder="Search" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Search"
IconSize="Size.Medium" Class="mt-0" Variant="Variant.Outlined" />
</div>
</ToolBarContent>
<ColGroup>
<col style="width:60px;" />
<col style="width:100px;" />
<col style="width:150px;" />
<col style="width:120px;" />
<col />
<col style="width:100px;" />
<col />
<col style="width:100px;" />
</ColGroup>
<HeaderContent>
<MudTh>STT</MudTh>
<MudTh>Tên</MudTh>
<MudTh>Id</MudTh>
<MudTh>Create at</MudTh>
<MudTh>Parameters</MudTh>
<MudTh>Status</MudTh>
<MudTh>Log</MudTh>
<MudTh></MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@context.Index</MudTd>
<MudTd DataLabel="Tên">@context.MissionName</MudTd>
<MudTd DataLabel="Id">@context.Id.ToString()[..13]</MudTd>
<MudTd DataLabel="Create at">@context.CreatedAt.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss")</MudTd>
<MudTd DataLabel="Parameters">
@if (context.Parameters?.Any() == true)
{
<span title="@context.Parameters">
@ShortenLog(context.Parameters, 30)
</span>
<MudIconButton Icon="@Icons.Material.Filled.Info" Color="Color.Primary" Size="Size.Small"
OnClick="@(() => ShowParametersDialog(context))" />
}
else
{
<span>{}</span>
}
</MudTd>
<MudTd DataLabel="Status">
@if (context.Status == MissionStatus.Running)
{
<MudChip T="string" Color="Color.Success" Variant="Variant.Filled">@context.Status</MudChip>
}
else if (context.Status == MissionStatus.Completed)
{
<MudChip T="string" Color="Color.Primary" Variant="Variant.Filled">@context.Status</MudChip>
}
else if (context.Status == MissionStatus.Error)
{
<MudChip T="string" Color="Color.Error" Variant="Variant.Filled">@context.Status</MudChip>
}
else
{
<MudChip T="string" Color="Color.Warning" Variant="Variant.Filled">@context.Status</MudChip>
}
</MudTd>
<MudTd DataLabel="Log">
@if (!string.IsNullOrEmpty(context.Log))
{
<span title="@context.Log">
@ShortenLog(context.Log, 30)
</span>
<MudIconButton Icon="@Icons.Material.Filled.Visibility" Color="Color.Primary" Size="Size.Small"
OnClick="@(() => ShowLogDialog(context.Log))" />
}
</MudTd>
<MudTd DataLabel="" Class="text-center">
@if (context.Status == MissionStatus.Running)
{
<MudIconButton Icon="@Icons.Material.Filled.Stop" Color="Color.Error" Size="Size.Small" OnClick="@(() => PauseMission(context))" />
}
else if (context.Status == MissionStatus.Paused)
{
<MudIconButton Icon="@Icons.Material.Filled.PlayArrow" Color="Color.Primary" Size="Size.Small" OnClick="@(() => ResumeMission(context))" />
}
@if (context.Status == MissionStatus.Running || context.Status == MissionStatus.Paused)
{
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" Size="Size.Small" OnClick="@(() => CancelMission(context))" />
}
</MudTd>
</RowTemplate>
<NoRecordsContent>
<MudText>No matching records found</MudText>
</NoRecordsContent>
<LoadingContent>
<MudText>Loading...</MudText>
</LoadingContent>
<PagerContent>
<MudTablePager />
</PagerContent>
</MudTable>
</div>
<MudDialog @bind-Visible="_logDialogOpen" Style="min-width: 900px;">
<DialogContent>
<MudText Typo="Typo.h6">Mission Log</MudText>
<MudPaper Class="pa-2" Style="overflow-x:auto;">
<pre style="white-space:pre-wrap; word-break:break-all;">@_selectedLog</pre>
</MudPaper>
</DialogContent>
<DialogActions>
<MudButton OnClick="() => _logDialogOpen = false" Color="Color.Primary">Close</MudButton>
</DialogActions>
</MudDialog>
<MudDialog @bind-Visible="_parametersDialogOpen" Style="min-width: 600px;">
<DialogContent>
<MudText Typo="Typo.h6">Parameters of mission "@_missionName"</MudText>
<MudPaper Class="pa-2" Style="overflow-x:auto;">
<table class="mud-table mud-table-dense" style="width:100%;">
<thead>
<tr>
<th style="text-align:left;">Key</th>
<th style="text-align:left;">Value</th>
</tr>
</thead>
<tbody>
@foreach (var kv in _selectedParameters)
{
<tr>
<td style="vertical-align:top;">@kv.Key</td>
<td style="vertical-align:top;">@kv.Value</td>
</tr>
}
</tbody>
</table>
</MudPaper>
</DialogContent>
<DialogActions>
<MudButton OnClick="() => _parametersDialogOpen = false" Color="Color.Primary">Close</MudButton>
</DialogActions>
</MudDialog>
@code {
private string TableHeight = "0px";
private ElementReference divRef;
private ElementReference toolbarRef;
private MudTable<InstanceMissionModel> table = default!;
private List<InstanceMissionModel> runnerMissions = [];
private string searchString = "";
private string _missionName = "";
private bool _parametersDialogOpen = false;
private IDictionary<string, string> _selectedParameters = new Dictionary<string, string>();
private bool _logDialogOpen = false;
private string _selectedLog = string.Empty;
private HttpClient http = default!;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (firstRender)
{
var rect = await divRef.MudGetBoundingClientRectAsync();
var toolbarRect = await toolbarRef.MudGetBoundingClientRectAsync();
TableHeight = $"{rect.Height - 70 - Math.Max(toolbarRect.Height, 64)}px";
StateHasChanged();
await table.ReloadServerData();
}
}
private async Task<TableData<InstanceMissionModel>> ServerReload(TableState state, CancellationToken token)
{
if (http == null)
{
http = httpFactory.CreateClient("ScriptManagerAPI");
}
var url = $"api/ScriptMissions/Runner?txtSearch={Uri.EscapeDataString(searchString)}&page={state.Page + 1}&size={state.PageSize}";
var response = await http.GetFromJsonAsync<SearchResult<InstanceMissionDto>>(url) ?? new();
return new TableData<InstanceMissionModel>()
{
TotalItems = response.Total,
Items = [.. response.Items.Select((item, index) => new InstanceMissionModel(index + 1 + state.Page * state.PageSize, item))]
};
}
private void ShowParametersDialog(InstanceMissionModel mission)
{
try
{
_selectedParameters = System.Text.Json.JsonSerializer.Deserialize<IDictionary<string, string>>(mission.Parameters)
?? throw new Exception($"Can not convert parameters data from \"{mission.Parameters}\"");
_missionName = mission.MissionName;
_parametersDialogOpen = true;
StateHasChanged();
}
catch (Exception ex)
{
Snackbar.Add($"Failed to parse parameters: {ex.Message}", Severity.Error);
}
}
private string ShortenLog(string? log, int maxLength)
{
if (string.IsNullOrEmpty(log)) return string.Empty;
return log.Length > maxLength ? log.Substring(0, maxLength) + "..." : log;
}
private void ShowLogDialog(string log)
{
_selectedLog = log;
_logDialogOpen = true;
StateHasChanged();
}
private async Task OnSearch(string text)
{
searchString = text;
await table.ReloadServerData();
}
private async Task CancelMission(InstanceMissionModel model)
{
var response = await http.DeleteFromJsonAsync<MessageResult>($"api/ScriptMissions/Runner/{model.Id}");
if (response == null)
{
Snackbar.Add("Failed to cancel mission: server response error", Severity.Error);
}
else if (response.IsSuccess)
{
Snackbar.Add("Mission canceled successfully", Severity.Success);
await table.ReloadServerData();
}
else
{
Snackbar.Add($"Failed to cancel mission: {response.Message}", Severity.Error);
}
}
private async Task PauseMission(InstanceMissionModel model)
{
var response = await http.PutFromJsonAsync<MessageResult>($"api/ScriptMissions/Runner/{model.Id}/pause", new object());
if (response == null)
{
Snackbar.Add("Failed to pause mission: server response error", Severity.Error);
}
else if (response.IsSuccess)
{
Snackbar.Add("Mission paused successfully", Severity.Success);
await table.ReloadServerData();
}
else
{
Snackbar.Add($"Failed to pause mission: {response.Message}", Severity.Error);
}
}
private async Task ResumeMission(InstanceMissionModel model)
{
var response = await http.PutFromJsonAsync<MessageResult>($"api/ScriptMissions/Runner/{model.Id}/resume", new object());
if (response == null)
{
Snackbar.Add("Failed to resume mission: server response error", Severity.Error);
}
else if (response.IsSuccess)
{
Snackbar.Add("Mission resumed successfully", Severity.Success);
await table.ReloadServerData();
}
else
{
Snackbar.Add($"Failed to resume mission: {response.Message}", Severity.Error);
}
}
private async Task ReloadTable()
{
await table.ReloadServerData();
}
}