305 lines
12 KiB
Plaintext
305 lines
12 KiB
Plaintext
@implements IAsyncDisposable
|
|
|
|
@using MudBlazor
|
|
@using Microsoft.JSInterop
|
|
@using Microsoft.AspNetCore.Components.Authorization
|
|
@using RobotNet10.ScriptEditor.Clients
|
|
@using RobotNet10.ScriptEditor.Dialogs
|
|
@using RobotNet10.ScriptEngine.Shared
|
|
@using RobotNet10.Shared
|
|
@using RobotNet10.Components
|
|
|
|
@inject InstanceMissionHubClient InstanceMissionClient
|
|
@inject IDialogService DialogService
|
|
@inject ISnackbar Snackbar
|
|
@inject IJSRuntime JSRuntime
|
|
@inject AuthenticationStateProvider AuthenticationStateProvider
|
|
|
|
<div class="w-100 h-100 p-3">
|
|
<div @ref="_containerRef" class="w-100 h-100">
|
|
<MudTable @ref="table" T="InstanceMissionDto"
|
|
ServerData="@(new Func<TableState, CancellationToken, Task<TableData<InstanceMissionDto>>>(LoadData))"
|
|
Hover="true"
|
|
Striped="true"
|
|
Dense="true"
|
|
Height="@_tableHeight"
|
|
Loading="@_isLoading">
|
|
<ToolBarContent>
|
|
<div @ref="toolbarRef" class="w-100 d-flex flex-row">
|
|
<MudText Typo="Typo.h6">Instance Missions</MudText>
|
|
<MudSpacer />
|
|
<MudIconButton Class="me-2" Icon="@Icons.Material.Filled.Refresh" Color="Color.Primary" Size="Size.Small" OnClick="OnSearch" />
|
|
<MudTextField T="string" Immediate="true" OnAdornmentClick="OnSearch" OnKeyDown="@(async (KeyboardEventArgs e) => { if (e.Key == "Enter") await OnSearch(); })"
|
|
OnDebounceIntervalElapsed="OnSearch" DebounceInterval="1000" Value="@_searchText" Margin="Margin.Dense"
|
|
Placeholder="Search" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Search"
|
|
IconSize="Size.Medium" Class="mt-0" Variant="Variant.Outlined" />
|
|
</div>
|
|
</ToolBarContent>
|
|
<HeaderContent>
|
|
<MudTh>Mission Name</MudTh>
|
|
<MudTh>State</MudTh>
|
|
<MudTh>Score</MudTh>
|
|
<MudTh>Created At</MudTh>
|
|
<MudTh>Stopped At</MudTh>
|
|
<MudTh>Actions</MudTh>
|
|
</HeaderContent>
|
|
<RowTemplate>
|
|
<MudTd DataLabel="Mission Name">@context.MissionName</MudTd>
|
|
<MudTd DataLabel="State">
|
|
<MudChip T="string" Size="Size.Small" Color="@GetStateColor(context.State)">
|
|
@context.State
|
|
</MudChip>
|
|
</MudTd>
|
|
<MudTd DataLabel="Score">@($"{(100.0 * context.Score / @context.TotalScore):#.00}%")</MudTd>
|
|
<MudTd DataLabel="Created At">@context.CreatedAt.ToString("yyyy-MM-dd HH:mm:ss")</MudTd>
|
|
<MudTd DataLabel="Created At">@(GetStoppedAtString(context))</MudTd>
|
|
<MudTd DataLabel="Actions">
|
|
<div style="display: flex; gap: 4px;">
|
|
<IconButton Icon="script-text"
|
|
Title="View Log"
|
|
OnClick="@(() => HandleViewLog(context))" />
|
|
@if (context.State == ScriptMissionState.Running || context.State == ScriptMissionState.Paused || context.State == ScriptMissionState.Pausing)
|
|
{
|
|
<IconButton Icon="cancel"
|
|
Title="Cancel Mission"
|
|
OnClick="@(() => HandleCancelMission(context))" />
|
|
}
|
|
@if (context.State == ScriptMissionState.Running)
|
|
{
|
|
<IconButton Icon="pause"
|
|
Title="Pause Mission"
|
|
OnClick="@(() => HandlePauseMission(context))" />
|
|
}
|
|
@if (context.State == ScriptMissionState.Paused)
|
|
{
|
|
<IconButton Icon="play"
|
|
Title="Resume Mission"
|
|
OnClick="@(() => HandleResumeMission(context))" />
|
|
}
|
|
</div>
|
|
</MudTd>
|
|
</RowTemplate>
|
|
<NoRecordsContent>
|
|
<MudText>No matching records found</MudText>
|
|
</NoRecordsContent>
|
|
<LoadingContent>
|
|
<MudText>Loading...</MudText>
|
|
</LoadingContent>
|
|
<PagerContent>
|
|
<MudTablePager />
|
|
</PagerContent>
|
|
</MudTable>
|
|
</div>
|
|
</div>
|
|
|
|
@code {
|
|
private ElementReference _containerRef;
|
|
private ElementReference toolbarRef;
|
|
private MudTable<InstanceMissionDto> table = default!;
|
|
private string _tableHeight = "400px";
|
|
private string _searchText = "";
|
|
private bool _isLoading = false;
|
|
private int _totalItems = 0;
|
|
|
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
{
|
|
await base.OnAfterRenderAsync(firstRender);
|
|
if (firstRender)
|
|
{
|
|
await InstanceMissionClient.StartAsync();
|
|
await CalculateTableHeight();
|
|
}
|
|
}
|
|
|
|
private async Task CalculateTableHeight()
|
|
{
|
|
var rect = await _containerRef.MudGetBoundingClientRectAsync();
|
|
var toolbarRect = await toolbarRef.MudGetBoundingClientRectAsync();
|
|
_tableHeight = $"{rect.Height - 70 - Math.Max(toolbarRect.Height, 64)}px";
|
|
StateHasChanged();
|
|
}
|
|
|
|
private async Task<TableData<InstanceMissionDto>> LoadData(TableState state, CancellationToken cancellationToken)
|
|
{
|
|
_isLoading = true;
|
|
StateHasChanged();
|
|
|
|
try
|
|
{
|
|
var request = new SearchRequest(
|
|
Page: state.Page + 1, // MudTable uses 0-based page, but our API uses 1-based
|
|
Size: state.PageSize,
|
|
TxtSearch: _searchText
|
|
);
|
|
|
|
var result = await InstanceMissionClient.SearchInstanceMissionsAsync(request);
|
|
_totalItems = result.Total;
|
|
|
|
return new TableData<InstanceMissionDto>
|
|
{
|
|
Items = result.Items,
|
|
TotalItems = result.Total
|
|
};
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"Error loading missions: {ex.Message}", Severity.Error);
|
|
return new TableData<InstanceMissionDto>
|
|
{
|
|
Items = [],
|
|
TotalItems = 0
|
|
};
|
|
}
|
|
finally
|
|
{
|
|
_isLoading = false;
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
|
|
private async Task OnSearch()
|
|
{
|
|
await table.ReloadServerData();
|
|
}
|
|
|
|
private Color GetStateColor(ScriptMissionState state)
|
|
{
|
|
return state switch
|
|
{
|
|
ScriptMissionState.Running => Color.Success,
|
|
ScriptMissionState.Paused => Color.Warning,
|
|
ScriptMissionState.Pausing => Color.Warning,
|
|
ScriptMissionState.Resuming => Color.Info,
|
|
ScriptMissionState.Completed => Color.Success,
|
|
ScriptMissionState.Canceled => Color.Default,
|
|
ScriptMissionState.Error => Color.Error,
|
|
_ => Color.Default
|
|
};
|
|
}
|
|
|
|
private async Task HandleViewLog(InstanceMissionDto mission)
|
|
{
|
|
var parameters = new DialogParameters<MissionLogDialog>
|
|
{
|
|
{ x => x.MissionId, mission.Id },
|
|
{ x => x.MissionName, mission.MissionName },
|
|
{ x => x.State, mission.State },
|
|
{ x => x.InitialLog, mission.Log }
|
|
};
|
|
|
|
var options = new DialogOptions
|
|
{
|
|
MaxWidth = MaxWidth.Large,
|
|
FullWidth = true,
|
|
CloseButton = true
|
|
};
|
|
|
|
await DialogService.ShowAsync<MissionLogDialog>($"Mission Log: {mission.MissionName}", parameters, options);
|
|
}
|
|
|
|
private async Task HandleCancelMission(InstanceMissionDto mission)
|
|
{
|
|
var parameters = new DialogParameters<CancelMissionDialog>
|
|
{
|
|
{ x => x.MissionName, mission.MissionName }
|
|
};
|
|
|
|
var options = new DialogOptions
|
|
{
|
|
MaxWidth = MaxWidth.Small,
|
|
FullWidth = true
|
|
};
|
|
|
|
var dialog = await DialogService.ShowAsync<CancelMissionDialog>("Cancel Mission", parameters, options);
|
|
var result = await dialog.Result;
|
|
|
|
if (result is not null && !result.Canceled && result.Data is string userReason)
|
|
{
|
|
try
|
|
{
|
|
// Get current user information
|
|
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
|
|
var userName = authState?.User?.Identity?.Name ?? "Unknown";
|
|
|
|
// Combine user reason with user information
|
|
var reason = string.IsNullOrWhiteSpace(userReason)
|
|
? $"Canceled by {userName}"
|
|
: $"Canceled by {userName}: {userReason.Trim()}";
|
|
|
|
var messageResult = await InstanceMissionClient.CancelMissionAsync(mission.Id, reason);
|
|
if (messageResult.IsSuccess)
|
|
{
|
|
Snackbar.Add(messageResult.Message ?? "Mission canceled", Severity.Success);
|
|
await table.ReloadServerData();
|
|
}
|
|
else
|
|
{
|
|
Snackbar.Add(messageResult.Message ?? "Failed to cancel mission", Severity.Error);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"Error canceling mission: {ex.Message}", Severity.Error);
|
|
}
|
|
}
|
|
}
|
|
|
|
private async Task HandlePauseMission(InstanceMissionDto mission)
|
|
{
|
|
try
|
|
{
|
|
var messageResult = await InstanceMissionClient.PauseMissionAsync(mission.Id);
|
|
if (messageResult.IsSuccess)
|
|
{
|
|
Snackbar.Add(messageResult.Message ?? "Mission paused", Severity.Success);
|
|
}
|
|
else
|
|
{
|
|
Snackbar.Add(messageResult.Message ?? "Failed to pause mission", Severity.Error);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"Error pausing mission: {ex.Message}", Severity.Error);
|
|
}
|
|
}
|
|
|
|
private async Task HandleResumeMission(InstanceMissionDto mission)
|
|
{
|
|
try
|
|
{
|
|
var messageResult = await InstanceMissionClient.ResumeMissionAsync(mission.Id);
|
|
if (messageResult.IsSuccess)
|
|
{
|
|
Snackbar.Add(messageResult.Message ?? "Mission resumed", Severity.Success);
|
|
}
|
|
else
|
|
{
|
|
Snackbar.Add(messageResult.Message ?? "Failed to resume mission", Severity.Error);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"Error resuming mission: {ex.Message}", Severity.Error);
|
|
}
|
|
}
|
|
|
|
private string GetStoppedAtString(InstanceMissionDto mission)
|
|
{
|
|
if (mission.State == ScriptMissionState.Canceled || mission.State == ScriptMissionState.Completed || mission.State == ScriptMissionState.Error)
|
|
{
|
|
return mission.StoppedAt.ToString("yyyy-MM-dd HH:mm:ss");
|
|
}
|
|
else
|
|
{
|
|
return "--";
|
|
}
|
|
}
|
|
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
await InstanceMissionClient.StopAsync();
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
}
|