Add admin page for bundle
This commit is contained in:
232
src/Managing.Api/Controllers/AdminController.cs
Normal file
232
src/Managing.Api/Controllers/AdminController.cs
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
using Managing.Api.Models.Responses;
|
||||||
|
using Managing.Application.Abstractions.Services;
|
||||||
|
using Managing.Application.Abstractions.Shared;
|
||||||
|
using Managing.Application.Shared;
|
||||||
|
using Managing.Domain.Backtests;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using static Managing.Common.Enums;
|
||||||
|
|
||||||
|
namespace Managing.Api.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Controller for admin operations.
|
||||||
|
/// Provides endpoints for administrative tasks that require admin authorization.
|
||||||
|
/// All endpoints in this controller require admin access.
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Authorize]
|
||||||
|
[Route("[controller]")]
|
||||||
|
[Produces("application/json")]
|
||||||
|
public class AdminController : BaseController
|
||||||
|
{
|
||||||
|
private readonly IBacktester _backtester;
|
||||||
|
private readonly IAdminConfigurationService _adminService;
|
||||||
|
private readonly ILogger<AdminController> _logger;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="AdminController"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userService">The service for user management.</param>
|
||||||
|
/// <param name="backtester">The service for backtesting operations.</param>
|
||||||
|
/// <param name="adminService">The admin configuration service for authorization checks.</param>
|
||||||
|
/// <param name="logger">The logger instance.</param>
|
||||||
|
public AdminController(
|
||||||
|
IUserService userService,
|
||||||
|
IBacktester backtester,
|
||||||
|
IAdminConfigurationService adminService,
|
||||||
|
ILogger<AdminController> logger) : base(userService)
|
||||||
|
{
|
||||||
|
_backtester = backtester;
|
||||||
|
_adminService = adminService;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if the current user is an admin
|
||||||
|
/// </summary>
|
||||||
|
private async Task<bool> IsUserAdmin()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var user = await GetUser();
|
||||||
|
if (user == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return await _adminService.IsUserAdminAsync(user.Name);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error checking if user is admin");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves paginated bundle backtest requests for admin users.
|
||||||
|
/// This endpoint returns all bundle backtest requests without user filtering.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="page">Page number (defaults to 1)</param>
|
||||||
|
/// <param name="pageSize">Number of items per page (defaults to 50, max 100)</param>
|
||||||
|
/// <param name="sortBy">Field to sort by (defaults to "CreatedAt")</param>
|
||||||
|
/// <param name="sortOrder">Sort order - "asc" or "desc" (defaults to "desc")</param>
|
||||||
|
/// <param name="nameContains">Filter by name contains</param>
|
||||||
|
/// <param name="status">Filter by status (Pending, Running, Completed, Failed, Saved)</param>
|
||||||
|
/// <param name="userId">Filter by user ID</param>
|
||||||
|
/// <param name="userNameContains">Filter by user name contains</param>
|
||||||
|
/// <param name="totalBacktestsMin">Filter by minimum total backtests</param>
|
||||||
|
/// <param name="totalBacktestsMax">Filter by maximum total backtests</param>
|
||||||
|
/// <param name="completedBacktestsMin">Filter by minimum completed backtests</param>
|
||||||
|
/// <param name="completedBacktestsMax">Filter by maximum completed backtests</param>
|
||||||
|
/// <param name="progressPercentageMin">Filter by minimum progress percentage (0-100)</param>
|
||||||
|
/// <param name="progressPercentageMax">Filter by maximum progress percentage (0-100)</param>
|
||||||
|
/// <param name="createdAtFrom">Filter by created date from</param>
|
||||||
|
/// <param name="createdAtTo">Filter by created date to</param>
|
||||||
|
/// <returns>A paginated list of bundle backtest requests.</returns>
|
||||||
|
[HttpGet]
|
||||||
|
[Route("BundleBacktestRequests/Paginated")]
|
||||||
|
public async Task<ActionResult<PaginatedBundleBacktestRequestsResponse>> GetBundleBacktestRequestsPaginated(
|
||||||
|
[FromQuery] int page = 1,
|
||||||
|
[FromQuery] int pageSize = 50,
|
||||||
|
[FromQuery] BundleBacktestRequestSortableColumn sortBy = BundleBacktestRequestSortableColumn.CreatedAt,
|
||||||
|
[FromQuery] string sortOrder = "desc",
|
||||||
|
[FromQuery] string? nameContains = null,
|
||||||
|
[FromQuery] BundleBacktestRequestStatus? status = null,
|
||||||
|
[FromQuery] int? userId = null,
|
||||||
|
[FromQuery] string? userNameContains = null,
|
||||||
|
[FromQuery] int? totalBacktestsMin = null,
|
||||||
|
[FromQuery] int? totalBacktestsMax = null,
|
||||||
|
[FromQuery] int? completedBacktestsMin = null,
|
||||||
|
[FromQuery] int? completedBacktestsMax = null,
|
||||||
|
[FromQuery] double? progressPercentageMin = null,
|
||||||
|
[FromQuery] double? progressPercentageMax = null,
|
||||||
|
[FromQuery] DateTime? createdAtFrom = null,
|
||||||
|
[FromQuery] DateTime? createdAtTo = null)
|
||||||
|
{
|
||||||
|
if (!await IsUserAdmin())
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Non-admin user attempted to access admin bundle backtest requests endpoint");
|
||||||
|
return StatusCode(403, new { error = "Only admin users can access this endpoint" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (page < 1)
|
||||||
|
{
|
||||||
|
return BadRequest("Page must be greater than 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pageSize < 1 || pageSize > 100)
|
||||||
|
{
|
||||||
|
return BadRequest("Page size must be between 1 and 100");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sortOrder != "asc" && sortOrder != "desc")
|
||||||
|
{
|
||||||
|
return BadRequest("Sort order must be 'asc' or 'desc'");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate progress percentage ranges [0,100]
|
||||||
|
if (progressPercentageMin.HasValue && (progressPercentageMin < 0 || progressPercentageMin > 100))
|
||||||
|
{
|
||||||
|
return BadRequest("progressPercentageMin must be between 0 and 100");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progressPercentageMax.HasValue && (progressPercentageMax < 0 || progressPercentageMax > 100))
|
||||||
|
{
|
||||||
|
return BadRequest("progressPercentageMax must be between 0 and 100");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progressPercentageMin.HasValue && progressPercentageMax.HasValue && progressPercentageMin > progressPercentageMax)
|
||||||
|
{
|
||||||
|
return BadRequest("progressPercentageMin must be less than or equal to progressPercentageMax");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build filter
|
||||||
|
var filter = new BundleBacktestRequestsFilter
|
||||||
|
{
|
||||||
|
NameContains = string.IsNullOrWhiteSpace(nameContains) ? null : nameContains.Trim(),
|
||||||
|
Status = status,
|
||||||
|
UserId = userId,
|
||||||
|
UserNameContains = string.IsNullOrWhiteSpace(userNameContains) ? null : userNameContains.Trim(),
|
||||||
|
TotalBacktestsMin = totalBacktestsMin,
|
||||||
|
TotalBacktestsMax = totalBacktestsMax,
|
||||||
|
CompletedBacktestsMin = completedBacktestsMin,
|
||||||
|
CompletedBacktestsMax = completedBacktestsMax,
|
||||||
|
ProgressPercentageMin = progressPercentageMin,
|
||||||
|
ProgressPercentageMax = progressPercentageMax,
|
||||||
|
CreatedAtFrom = createdAtFrom,
|
||||||
|
CreatedAtTo = createdAtTo
|
||||||
|
};
|
||||||
|
|
||||||
|
var (bundleRequests, totalCount) =
|
||||||
|
await _backtester.GetBundleBacktestRequestsPaginatedAsync(
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
sortBy,
|
||||||
|
sortOrder,
|
||||||
|
filter);
|
||||||
|
|
||||||
|
var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
|
||||||
|
|
||||||
|
var response = new PaginatedBundleBacktestRequestsResponse
|
||||||
|
{
|
||||||
|
BundleRequests = bundleRequests.Select(b => new BundleBacktestRequestListItemResponse
|
||||||
|
{
|
||||||
|
RequestId = b.RequestId,
|
||||||
|
Name = b.Name,
|
||||||
|
Version = b.Version,
|
||||||
|
Status = b.Status.ToString(),
|
||||||
|
CreatedAt = b.CreatedAt,
|
||||||
|
CompletedAt = b.CompletedAt,
|
||||||
|
UpdatedAt = b.UpdatedAt,
|
||||||
|
TotalBacktests = b.TotalBacktests,
|
||||||
|
CompletedBacktests = b.CompletedBacktests,
|
||||||
|
FailedBacktests = b.FailedBacktests,
|
||||||
|
ProgressPercentage = b.ProgressPercentage,
|
||||||
|
UserId = b.User?.Id,
|
||||||
|
UserName = b.User?.Name,
|
||||||
|
ErrorMessage = b.ErrorMessage,
|
||||||
|
CurrentBacktest = b.CurrentBacktest,
|
||||||
|
EstimatedTimeRemainingSeconds = b.EstimatedTimeRemainingSeconds
|
||||||
|
}),
|
||||||
|
TotalCount = totalCount,
|
||||||
|
CurrentPage = page,
|
||||||
|
PageSize = pageSize,
|
||||||
|
TotalPages = totalPages,
|
||||||
|
HasNextPage = page < totalPages,
|
||||||
|
HasPreviousPage = page > 1
|
||||||
|
};
|
||||||
|
|
||||||
|
return Ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a summary of bundle backtest requests grouped by status with counts.
|
||||||
|
/// Admin only endpoint.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Summary statistics of bundle backtest requests</returns>
|
||||||
|
[HttpGet]
|
||||||
|
[Route("BundleBacktestRequests/Summary")]
|
||||||
|
public async Task<ActionResult<BundleBacktestRequestSummaryResponse>> GetBundleBacktestRequestsSummary()
|
||||||
|
{
|
||||||
|
if (!await IsUserAdmin())
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Non-admin user attempted to get bundle backtest requests summary");
|
||||||
|
return StatusCode(403, new { error = "Only admin users can access bundle backtest requests summary" });
|
||||||
|
}
|
||||||
|
|
||||||
|
var summary = await _backtester.GetBundleBacktestRequestsSummaryAsync();
|
||||||
|
|
||||||
|
var response = new BundleBacktestRequestSummaryResponse
|
||||||
|
{
|
||||||
|
StatusSummary = summary.StatusCounts.Select(s => new BundleBacktestRequestStatusSummary
|
||||||
|
{
|
||||||
|
Status = s.Status.ToString(),
|
||||||
|
Count = s.Count
|
||||||
|
}).ToList(),
|
||||||
|
TotalRequests = summary.TotalRequests
|
||||||
|
};
|
||||||
|
|
||||||
|
return Ok(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
#nullable enable
|
||||||
|
namespace Managing.Api.Models.Responses;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Response model for bundle backtest request summary statistics
|
||||||
|
/// </summary>
|
||||||
|
public class BundleBacktestRequestSummaryResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Summary of bundle requests by status
|
||||||
|
/// </summary>
|
||||||
|
public List<BundleBacktestRequestStatusSummary> StatusSummary { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Total number of bundle backtest requests
|
||||||
|
/// </summary>
|
||||||
|
public int TotalRequests { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Summary of bundle backtest requests by status
|
||||||
|
/// </summary>
|
||||||
|
public class BundleBacktestRequestStatusSummary
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The status name
|
||||||
|
/// </summary>
|
||||||
|
public string Status { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The count of bundle requests with this status
|
||||||
|
/// </summary>
|
||||||
|
public int Count { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
#nullable enable
|
||||||
|
namespace Managing.Api.Models.Responses;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Response model for paginated bundle backtest requests
|
||||||
|
/// </summary>
|
||||||
|
public class PaginatedBundleBacktestRequestsResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The list of bundle backtest requests for the current page
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<BundleBacktestRequestListItemResponse> BundleRequests { get; set; } = new List<BundleBacktestRequestListItemResponse>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Total number of bundle backtest requests across all pages
|
||||||
|
/// </summary>
|
||||||
|
public int TotalCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Current page number
|
||||||
|
/// </summary>
|
||||||
|
public int CurrentPage { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Number of items per page
|
||||||
|
/// </summary>
|
||||||
|
public int PageSize { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Total number of pages
|
||||||
|
/// </summary>
|
||||||
|
public int TotalPages { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether there are more pages available
|
||||||
|
/// </summary>
|
||||||
|
public bool HasNextPage { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether there are previous pages available
|
||||||
|
/// </summary>
|
||||||
|
public bool HasPreviousPage { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Response model for a bundle backtest request list item (summary view)
|
||||||
|
/// </summary>
|
||||||
|
public class BundleBacktestRequestListItemResponse
|
||||||
|
{
|
||||||
|
public Guid RequestId { get; set; }
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public int Version { get; set; }
|
||||||
|
public string Status { get; set; } = string.Empty;
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public DateTime? CompletedAt { get; set; }
|
||||||
|
public DateTime UpdatedAt { get; set; }
|
||||||
|
public int TotalBacktests { get; set; }
|
||||||
|
public int CompletedBacktests { get; set; }
|
||||||
|
public int FailedBacktests { get; set; }
|
||||||
|
public double ProgressPercentage { get; set; }
|
||||||
|
public int? UserId { get; set; }
|
||||||
|
public string? UserName { get; set; }
|
||||||
|
public string? ErrorMessage { get; set; }
|
||||||
|
public string? CurrentBacktest { get; set; }
|
||||||
|
public int? EstimatedTimeRemainingSeconds { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
using Managing.Domain.Backtests;
|
||||||
|
|
||||||
|
namespace Managing.Application.Abstractions.Repositories;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Summary statistics for bundle backtest requests
|
||||||
|
/// </summary>
|
||||||
|
public class BundleBacktestRequestSummary
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Counts of bundle requests by status
|
||||||
|
/// </summary>
|
||||||
|
public List<BundleBacktestRequestStatusCount> StatusCounts { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Total number of bundle backtest requests
|
||||||
|
/// </summary>
|
||||||
|
public int TotalRequests { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Count of bundle backtest requests by status
|
||||||
|
/// </summary>
|
||||||
|
public class BundleBacktestRequestStatusCount
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The status
|
||||||
|
/// </summary>
|
||||||
|
public BundleBacktestRequestStatus Status { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The count of bundle requests with this status
|
||||||
|
/// </summary>
|
||||||
|
public int Count { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
@@ -57,4 +57,22 @@ public interface IBacktestRepository
|
|||||||
Task DeleteBundleBacktestRequestByIdForUserAsync(User user, Guid id);
|
Task DeleteBundleBacktestRequestByIdForUserAsync(User user, Guid id);
|
||||||
IEnumerable<BundleBacktestRequest> GetBundleBacktestRequestsByStatus(BundleBacktestRequestStatus status);
|
IEnumerable<BundleBacktestRequest> GetBundleBacktestRequestsByStatus(BundleBacktestRequestStatus status);
|
||||||
Task<IEnumerable<BundleBacktestRequest>> GetBundleBacktestRequestsByStatusAsync(BundleBacktestRequestStatus status);
|
Task<IEnumerable<BundleBacktestRequest>> GetBundleBacktestRequestsByStatusAsync(BundleBacktestRequestStatus status);
|
||||||
|
|
||||||
|
// Admin methods - no user filter
|
||||||
|
(IEnumerable<BundleBacktestRequest> BundleRequests, int TotalCount) GetBundleBacktestRequestsPaginated(
|
||||||
|
int page,
|
||||||
|
int pageSize,
|
||||||
|
Enums.BundleBacktestRequestSortableColumn sortBy = Enums.BundleBacktestRequestSortableColumn.CreatedAt,
|
||||||
|
string sortOrder = "desc",
|
||||||
|
BundleBacktestRequestsFilter? filter = null);
|
||||||
|
|
||||||
|
Task<(IEnumerable<BundleBacktestRequest> BundleRequests, int TotalCount)> GetBundleBacktestRequestsPaginatedAsync(
|
||||||
|
int page,
|
||||||
|
int pageSize,
|
||||||
|
Enums.BundleBacktestRequestSortableColumn sortBy = Enums.BundleBacktestRequestSortableColumn.CreatedAt,
|
||||||
|
string sortOrder = "desc",
|
||||||
|
BundleBacktestRequestsFilter? filter = null);
|
||||||
|
|
||||||
|
// Admin summary methods
|
||||||
|
Task<BundleBacktestRequestSummary> GetBundleBacktestRequestsSummaryAsync();
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using Managing.Application.Abstractions.Shared;
|
using Managing.Application.Abstractions.Repositories;
|
||||||
|
using Managing.Application.Abstractions.Shared;
|
||||||
using Managing.Common;
|
using Managing.Common;
|
||||||
using Managing.Domain.Backtests;
|
using Managing.Domain.Backtests;
|
||||||
using Managing.Domain.Bots;
|
using Managing.Domain.Bots;
|
||||||
@@ -98,6 +99,21 @@ namespace Managing.Application.Abstractions.Services
|
|||||||
IEnumerable<BundleBacktestRequest> GetBundleBacktestRequestsByStatus(BundleBacktestRequestStatus status);
|
IEnumerable<BundleBacktestRequest> GetBundleBacktestRequestsByStatus(BundleBacktestRequestStatus status);
|
||||||
Task<IEnumerable<BundleBacktestRequest>> GetBundleBacktestRequestsByStatusAsync(BundleBacktestRequestStatus status);
|
Task<IEnumerable<BundleBacktestRequest>> GetBundleBacktestRequestsByStatusAsync(BundleBacktestRequestStatus status);
|
||||||
|
|
||||||
|
// Admin methods - no user filter
|
||||||
|
(IEnumerable<BundleBacktestRequest> BundleRequests, int TotalCount) GetBundleBacktestRequestsPaginated(
|
||||||
|
int page,
|
||||||
|
int pageSize,
|
||||||
|
Enums.BundleBacktestRequestSortableColumn sortBy = Enums.BundleBacktestRequestSortableColumn.CreatedAt,
|
||||||
|
string sortOrder = "desc",
|
||||||
|
BundleBacktestRequestsFilter? filter = null);
|
||||||
|
|
||||||
|
Task<(IEnumerable<BundleBacktestRequest> BundleRequests, int TotalCount)> GetBundleBacktestRequestsPaginatedAsync(
|
||||||
|
int page,
|
||||||
|
int pageSize,
|
||||||
|
Enums.BundleBacktestRequestSortableColumn sortBy = Enums.BundleBacktestRequestSortableColumn.CreatedAt,
|
||||||
|
string sortOrder = "desc",
|
||||||
|
BundleBacktestRequestsFilter? filter = null);
|
||||||
|
|
||||||
|
Task<BundleBacktestRequestSummary> GetBundleBacktestRequestsSummaryAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
using Managing.Domain.Backtests;
|
||||||
|
|
||||||
|
namespace Managing.Application.Abstractions.Shared;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Filter model for bundle backtest requests
|
||||||
|
/// </summary>
|
||||||
|
public class BundleBacktestRequestsFilter
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Filter by name contains (case-insensitive)
|
||||||
|
/// </summary>
|
||||||
|
public string? NameContains { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Filter by status
|
||||||
|
/// </summary>
|
||||||
|
public BundleBacktestRequestStatus? Status { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Filter by user ID
|
||||||
|
/// </summary>
|
||||||
|
public int? UserId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Filter by user name contains (case-insensitive)
|
||||||
|
/// </summary>
|
||||||
|
public string? UserNameContains { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Filter by minimum total backtests
|
||||||
|
/// </summary>
|
||||||
|
public int? TotalBacktestsMin { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Filter by maximum total backtests
|
||||||
|
/// </summary>
|
||||||
|
public int? TotalBacktestsMax { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Filter by minimum completed backtests
|
||||||
|
/// </summary>
|
||||||
|
public int? CompletedBacktestsMin { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Filter by maximum completed backtests
|
||||||
|
/// </summary>
|
||||||
|
public int? CompletedBacktestsMax { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Filter by minimum progress percentage (0-100)
|
||||||
|
/// </summary>
|
||||||
|
public double? ProgressPercentageMin { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Filter by maximum progress percentage (0-100)
|
||||||
|
/// </summary>
|
||||||
|
public double? ProgressPercentageMax { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Filter by created date from
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? CreatedAtFrom { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Filter by created date to
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? CreatedAtTo { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Managing.Application.Abstractions.Repositories;
|
||||||
using Managing.Application.Abstractions.Services;
|
using Managing.Application.Abstractions.Services;
|
||||||
using Managing.Application.Abstractions.Shared;
|
using Managing.Application.Abstractions.Shared;
|
||||||
using Managing.Domain.Backtests;
|
using Managing.Domain.Backtests;
|
||||||
@@ -91,34 +92,109 @@ public class BacktestExecutorAdapter : IBacktester
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Methods not needed for compute worker - throw NotImplementedException
|
// Methods not needed for compute worker - throw NotImplementedException
|
||||||
public Task<bool> DeleteBacktestAsync(string id) => throw new NotImplementedException("Not available in compute worker");
|
public Task<bool> DeleteBacktestAsync(string id) =>
|
||||||
public bool DeleteBacktests() => throw new NotImplementedException("Not available in compute worker");
|
throw new NotImplementedException("Not available in compute worker");
|
||||||
public IEnumerable<Backtest> GetBacktestsByUser(User user) => throw new NotImplementedException("Not available in compute worker");
|
|
||||||
public Task<IEnumerable<Backtest>> GetBacktestsByUserAsync(User user) => throw new NotImplementedException("Not available in compute worker");
|
|
||||||
public IEnumerable<Backtest> GetBacktestsByRequestId(Guid requestId) => throw new NotImplementedException("Not available in compute worker");
|
|
||||||
public Task<IEnumerable<Backtest>> GetBacktestsByRequestIdAsync(Guid requestId) => throw new NotImplementedException("Not available in compute worker");
|
|
||||||
public (IEnumerable<LightBacktest> Backtests, int TotalCount) GetBacktestsByRequestIdPaginated(Guid requestId, int page, int pageSize, string sortBy = "score", string sortOrder = "desc") => throw new NotImplementedException("Not available in compute worker");
|
|
||||||
public Task<(IEnumerable<LightBacktest> Backtests, int TotalCount)> GetBacktestsByRequestIdPaginatedAsync(Guid requestId, int page, int pageSize, string sortBy = "score", string sortOrder = "desc") => throw new NotImplementedException("Not available in compute worker");
|
|
||||||
public Task<Backtest> GetBacktestByIdForUserAsync(User user, string id) => throw new NotImplementedException("Not available in compute worker");
|
|
||||||
public Task<bool> DeleteBacktestByUserAsync(User user, string id) => throw new NotImplementedException("Not available in compute worker");
|
|
||||||
public Task<bool> DeleteBacktestsByIdsForUserAsync(User user, IEnumerable<string> ids) => throw new NotImplementedException("Not available in compute worker");
|
|
||||||
public bool DeleteBacktestsByUser(User user) => throw new NotImplementedException("Not available in compute worker");
|
|
||||||
public (IEnumerable<LightBacktest> Backtests, int TotalCount) GetBacktestsByUserPaginated(User user, int page, int pageSize, BacktestSortableColumn sortBy, string sortOrder = "desc", BacktestsFilter? filter = null) => throw new NotImplementedException("Not available in compute worker");
|
|
||||||
public Task<(IEnumerable<LightBacktest> Backtests, int TotalCount)> GetBacktestsByUserPaginatedAsync(User user, int page, int pageSize, BacktestSortableColumn sortBy, string sortOrder = "desc", BacktestsFilter? filter = null) => throw new NotImplementedException("Not available in compute worker");
|
|
||||||
public Task<bool> DeleteBacktestsByRequestIdAsync(Guid requestId) => throw new NotImplementedException("Not available in compute worker");
|
|
||||||
public Task<int> DeleteBacktestsByFiltersAsync(User user, BacktestsFilter filter) => throw new NotImplementedException("Not available in compute worker");
|
|
||||||
public Task InsertBundleBacktestRequestForUserAsync(User user, BundleBacktestRequest bundleRequest, bool saveAsTemplate = false) => throw new NotImplementedException("Not available in compute worker");
|
|
||||||
public Task SaveBundleBacktestRequestAsync(User user, BundleBacktestRequest bundleRequest) => throw new NotImplementedException("Not available in compute worker");
|
|
||||||
public Task CreateJobsForBundleRequestAsync(BundleBacktestRequest bundleRequest) => throw new NotImplementedException("Not available in compute worker");
|
|
||||||
public IEnumerable<BundleBacktestRequest> GetBundleBacktestRequestsByUser(User user) => throw new NotImplementedException("Not available in compute worker");
|
|
||||||
public Task<IEnumerable<BundleBacktestRequest>> GetBundleBacktestRequestsByUserAsync(User user) => throw new NotImplementedException("Not available in compute worker");
|
|
||||||
public BundleBacktestRequest? GetBundleBacktestRequestByIdForUser(User user, Guid id) => throw new NotImplementedException("Not available in compute worker");
|
|
||||||
public Task<BundleBacktestRequest?> GetBundleBacktestRequestByIdForUserAsync(User user, Guid id) => throw new NotImplementedException("Not available in compute worker");
|
|
||||||
public void UpdateBundleBacktestRequest(BundleBacktestRequest bundleRequest) => throw new NotImplementedException("Not available in compute worker");
|
|
||||||
public Task UpdateBundleBacktestRequestAsync(BundleBacktestRequest bundleRequest) => throw new NotImplementedException("Not available in compute worker");
|
|
||||||
public void DeleteBundleBacktestRequestByIdForUser(User user, Guid id) => throw new NotImplementedException("Not available in compute worker");
|
|
||||||
public Task DeleteBundleBacktestRequestByIdForUserAsync(User user, Guid id) => throw new NotImplementedException("Not available in compute worker");
|
|
||||||
public IEnumerable<BundleBacktestRequest> GetBundleBacktestRequestsByStatus(BundleBacktestRequestStatus status) => throw new NotImplementedException("Not available in compute worker");
|
|
||||||
public Task<IEnumerable<BundleBacktestRequest>> GetBundleBacktestRequestsByStatusAsync(BundleBacktestRequestStatus status) => throw new NotImplementedException("Not available in compute worker");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
public bool DeleteBacktests() => throw new NotImplementedException("Not available in compute worker");
|
||||||
|
|
||||||
|
public IEnumerable<Backtest> GetBacktestsByUser(User user) =>
|
||||||
|
throw new NotImplementedException("Not available in compute worker");
|
||||||
|
|
||||||
|
public Task<IEnumerable<Backtest>> GetBacktestsByUserAsync(User user) =>
|
||||||
|
throw new NotImplementedException("Not available in compute worker");
|
||||||
|
|
||||||
|
public IEnumerable<Backtest> GetBacktestsByRequestId(Guid requestId) =>
|
||||||
|
throw new NotImplementedException("Not available in compute worker");
|
||||||
|
|
||||||
|
public Task<IEnumerable<Backtest>> GetBacktestsByRequestIdAsync(Guid requestId) =>
|
||||||
|
throw new NotImplementedException("Not available in compute worker");
|
||||||
|
|
||||||
|
public (IEnumerable<LightBacktest> Backtests, int TotalCount) GetBacktestsByRequestIdPaginated(Guid requestId,
|
||||||
|
int page, int pageSize, string sortBy = "score", string sortOrder = "desc") =>
|
||||||
|
throw new NotImplementedException("Not available in compute worker");
|
||||||
|
|
||||||
|
public Task<(IEnumerable<LightBacktest> Backtests, int TotalCount)> GetBacktestsByRequestIdPaginatedAsync(
|
||||||
|
Guid requestId, int page, int pageSize, string sortBy = "score", string sortOrder = "desc") =>
|
||||||
|
throw new NotImplementedException("Not available in compute worker");
|
||||||
|
|
||||||
|
public Task<Backtest> GetBacktestByIdForUserAsync(User user, string id) =>
|
||||||
|
throw new NotImplementedException("Not available in compute worker");
|
||||||
|
|
||||||
|
public Task<bool> DeleteBacktestByUserAsync(User user, string id) =>
|
||||||
|
throw new NotImplementedException("Not available in compute worker");
|
||||||
|
|
||||||
|
public Task<bool> DeleteBacktestsByIdsForUserAsync(User user, IEnumerable<string> ids) =>
|
||||||
|
throw new NotImplementedException("Not available in compute worker");
|
||||||
|
|
||||||
|
public bool DeleteBacktestsByUser(User user) =>
|
||||||
|
throw new NotImplementedException("Not available in compute worker");
|
||||||
|
|
||||||
|
public (IEnumerable<LightBacktest> Backtests, int TotalCount) GetBacktestsByUserPaginated(User user, int page,
|
||||||
|
int pageSize, BacktestSortableColumn sortBy, string sortOrder = "desc", BacktestsFilter? filter = null) =>
|
||||||
|
throw new NotImplementedException("Not available in compute worker");
|
||||||
|
|
||||||
|
public Task<(IEnumerable<LightBacktest> Backtests, int TotalCount)> GetBacktestsByUserPaginatedAsync(User user,
|
||||||
|
int page, int pageSize, BacktestSortableColumn sortBy, string sortOrder = "desc",
|
||||||
|
BacktestsFilter? filter = null) => throw new NotImplementedException("Not available in compute worker");
|
||||||
|
|
||||||
|
public Task<bool> DeleteBacktestsByRequestIdAsync(Guid requestId) =>
|
||||||
|
throw new NotImplementedException("Not available in compute worker");
|
||||||
|
|
||||||
|
public Task<int> DeleteBacktestsByFiltersAsync(User user, BacktestsFilter filter) =>
|
||||||
|
throw new NotImplementedException("Not available in compute worker");
|
||||||
|
|
||||||
|
public Task InsertBundleBacktestRequestForUserAsync(User user, BundleBacktestRequest bundleRequest,
|
||||||
|
bool saveAsTemplate = false) => throw new NotImplementedException("Not available in compute worker");
|
||||||
|
|
||||||
|
public Task SaveBundleBacktestRequestAsync(User user, BundleBacktestRequest bundleRequest) =>
|
||||||
|
throw new NotImplementedException("Not available in compute worker");
|
||||||
|
|
||||||
|
public Task CreateJobsForBundleRequestAsync(BundleBacktestRequest bundleRequest) =>
|
||||||
|
throw new NotImplementedException("Not available in compute worker");
|
||||||
|
|
||||||
|
public IEnumerable<BundleBacktestRequest> GetBundleBacktestRequestsByUser(User user) =>
|
||||||
|
throw new NotImplementedException("Not available in compute worker");
|
||||||
|
|
||||||
|
public Task<IEnumerable<BundleBacktestRequest>> GetBundleBacktestRequestsByUserAsync(User user) =>
|
||||||
|
throw new NotImplementedException("Not available in compute worker");
|
||||||
|
|
||||||
|
public BundleBacktestRequest? GetBundleBacktestRequestByIdForUser(User user, Guid id) =>
|
||||||
|
throw new NotImplementedException("Not available in compute worker");
|
||||||
|
|
||||||
|
public Task<BundleBacktestRequest?> GetBundleBacktestRequestByIdForUserAsync(User user, Guid id) =>
|
||||||
|
throw new NotImplementedException("Not available in compute worker");
|
||||||
|
|
||||||
|
public void UpdateBundleBacktestRequest(BundleBacktestRequest bundleRequest) =>
|
||||||
|
throw new NotImplementedException("Not available in compute worker");
|
||||||
|
|
||||||
|
public Task UpdateBundleBacktestRequestAsync(BundleBacktestRequest bundleRequest) =>
|
||||||
|
throw new NotImplementedException("Not available in compute worker");
|
||||||
|
|
||||||
|
public void DeleteBundleBacktestRequestByIdForUser(User user, Guid id) =>
|
||||||
|
throw new NotImplementedException("Not available in compute worker");
|
||||||
|
|
||||||
|
public Task DeleteBundleBacktestRequestByIdForUserAsync(User user, Guid id) =>
|
||||||
|
throw new NotImplementedException("Not available in compute worker");
|
||||||
|
|
||||||
|
public IEnumerable<BundleBacktestRequest> GetBundleBacktestRequestsByStatus(BundleBacktestRequestStatus status) =>
|
||||||
|
throw new NotImplementedException("Not available in compute worker");
|
||||||
|
|
||||||
|
public Task<IEnumerable<BundleBacktestRequest>>
|
||||||
|
GetBundleBacktestRequestsByStatusAsync(BundleBacktestRequestStatus status) =>
|
||||||
|
throw new NotImplementedException("Not available in compute worker");
|
||||||
|
|
||||||
|
public (IEnumerable<BundleBacktestRequest> BundleRequests, int TotalCount) GetBundleBacktestRequestsPaginated(
|
||||||
|
int page, int pageSize,
|
||||||
|
BundleBacktestRequestSortableColumn sortBy = BundleBacktestRequestSortableColumn.CreatedAt,
|
||||||
|
string sortOrder = "desc", BundleBacktestRequestsFilter? filter = null) =>
|
||||||
|
throw new NotImplementedException("Not available in compute worker");
|
||||||
|
|
||||||
|
public Task<(IEnumerable<BundleBacktestRequest> BundleRequests, int TotalCount)>
|
||||||
|
GetBundleBacktestRequestsPaginatedAsync(int page, int pageSize,
|
||||||
|
BundleBacktestRequestSortableColumn sortBy = BundleBacktestRequestSortableColumn.CreatedAt,
|
||||||
|
string sortOrder = "desc", BundleBacktestRequestsFilter? filter = null) =>
|
||||||
|
throw new NotImplementedException("Not available in compute worker");
|
||||||
|
|
||||||
|
public Task<BundleBacktestRequestSummary> GetBundleBacktestRequestsSummaryAsync() =>
|
||||||
|
throw new NotImplementedException("Not available in compute worker");
|
||||||
|
}
|
||||||
@@ -561,6 +561,35 @@ namespace Managing.Application.Backtests
|
|||||||
return await _backtestRepository.GetBundleBacktestRequestsByStatusAsync(status);
|
return await _backtestRepository.GetBundleBacktestRequestsByStatusAsync(status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public (IEnumerable<BundleBacktestRequest> BundleRequests, int TotalCount) GetBundleBacktestRequestsPaginated(
|
||||||
|
int page,
|
||||||
|
int pageSize,
|
||||||
|
BundleBacktestRequestSortableColumn sortBy = BundleBacktestRequestSortableColumn.CreatedAt,
|
||||||
|
string sortOrder = "desc",
|
||||||
|
BundleBacktestRequestsFilter? filter = null)
|
||||||
|
{
|
||||||
|
var (bundleRequests, totalCount) =
|
||||||
|
_backtestRepository.GetBundleBacktestRequestsPaginated(page, pageSize, sortBy, sortOrder, filter);
|
||||||
|
return (bundleRequests, totalCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(IEnumerable<BundleBacktestRequest> BundleRequests, int TotalCount)> GetBundleBacktestRequestsPaginatedAsync(
|
||||||
|
int page,
|
||||||
|
int pageSize,
|
||||||
|
BundleBacktestRequestSortableColumn sortBy = BundleBacktestRequestSortableColumn.CreatedAt,
|
||||||
|
string sortOrder = "desc",
|
||||||
|
BundleBacktestRequestsFilter? filter = null)
|
||||||
|
{
|
||||||
|
var (bundleRequests, totalCount) =
|
||||||
|
await _backtestRepository.GetBundleBacktestRequestsPaginatedAsync(page, pageSize, sortBy, sortOrder, filter);
|
||||||
|
return (bundleRequests, totalCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<BundleBacktestRequestSummary> GetBundleBacktestRequestsSummaryAsync()
|
||||||
|
{
|
||||||
|
return await _backtestRepository.GetBundleBacktestRequestsSummaryAsync();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sends a LightBacktestResponse to all SignalR subscribers of a bundle request.
|
/// Sends a LightBacktestResponse to all SignalR subscribers of a bundle request.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -524,6 +524,25 @@ public static class Enums
|
|||||||
Name
|
Name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sortable columns for bundle backtest requests pagination endpoints
|
||||||
|
/// </summary>
|
||||||
|
public enum BundleBacktestRequestSortableColumn
|
||||||
|
{
|
||||||
|
RequestId,
|
||||||
|
Name,
|
||||||
|
Status,
|
||||||
|
CreatedAt,
|
||||||
|
CompletedAt,
|
||||||
|
TotalBacktests,
|
||||||
|
CompletedBacktests,
|
||||||
|
FailedBacktests,
|
||||||
|
ProgressPercentage,
|
||||||
|
UserId,
|
||||||
|
UserName,
|
||||||
|
UpdatedAt
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Event types for agent summary updates
|
/// Event types for agent summary updates
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ public class BundleBacktestRequest
|
|||||||
{
|
{
|
||||||
RequestId = Guid.NewGuid();
|
RequestId = Guid.NewGuid();
|
||||||
CreatedAt = DateTime.UtcNow;
|
CreatedAt = DateTime.UtcNow;
|
||||||
|
UpdatedAt = DateTime.UtcNow;
|
||||||
Status = BundleBacktestRequestStatus.Pending;
|
Status = BundleBacktestRequestStatus.Pending;
|
||||||
Results = new List<string>();
|
Results = new List<string>();
|
||||||
UniversalConfigJson = string.Empty;
|
UniversalConfigJson = string.Empty;
|
||||||
@@ -29,6 +30,7 @@ public class BundleBacktestRequest
|
|||||||
{
|
{
|
||||||
RequestId = requestId;
|
RequestId = requestId;
|
||||||
CreatedAt = DateTime.UtcNow;
|
CreatedAt = DateTime.UtcNow;
|
||||||
|
UpdatedAt = DateTime.UtcNow;
|
||||||
Status = BundleBacktestRequestStatus.Pending;
|
Status = BundleBacktestRequestStatus.Pending;
|
||||||
Results = new List<string>();
|
Results = new List<string>();
|
||||||
UniversalConfigJson = string.Empty;
|
UniversalConfigJson = string.Empty;
|
||||||
@@ -149,6 +151,12 @@ public class BundleBacktestRequest
|
|||||||
/// Estimated time remaining in seconds
|
/// Estimated time remaining in seconds
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? EstimatedTimeRemainingSeconds { get; set; }
|
public int? EstimatedTimeRemainingSeconds { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When the request was last updated
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -948,4 +948,357 @@ public class PostgreSqlBacktestRepository : IBacktestRepository
|
|||||||
|
|
||||||
return entities.Select(PostgreSqlMappers.Map);
|
return entities.Select(PostgreSqlMappers.Map);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public (IEnumerable<BundleBacktestRequest> BundleRequests, int TotalCount) GetBundleBacktestRequestsPaginated(
|
||||||
|
int page,
|
||||||
|
int pageSize,
|
||||||
|
BundleBacktestRequestSortableColumn sortBy = BundleBacktestRequestSortableColumn.CreatedAt,
|
||||||
|
string sortOrder = "desc",
|
||||||
|
BundleBacktestRequestsFilter? filter = null)
|
||||||
|
{
|
||||||
|
var baseQuery = _context.BundleBacktestRequests
|
||||||
|
.AsNoTracking()
|
||||||
|
.Include(b => b.User)
|
||||||
|
.AsQueryable();
|
||||||
|
|
||||||
|
// Apply filters
|
||||||
|
if (filter != null)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(filter.NameContains))
|
||||||
|
{
|
||||||
|
var nameLike = $"%{filter.NameContains.Trim()}%";
|
||||||
|
baseQuery = baseQuery.Where(b => EF.Functions.ILike(b.Name, nameLike));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.Status.HasValue)
|
||||||
|
{
|
||||||
|
baseQuery = baseQuery.Where(b => b.Status == filter.Status.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.UserId.HasValue)
|
||||||
|
{
|
||||||
|
baseQuery = baseQuery.Where(b => b.UserId == filter.UserId.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(filter.UserNameContains))
|
||||||
|
{
|
||||||
|
var userNameLike = $"%{filter.UserNameContains.Trim()}%";
|
||||||
|
baseQuery = baseQuery.Where(b => b.User != null && EF.Functions.ILike(b.User.Name, userNameLike));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.TotalBacktestsMin.HasValue)
|
||||||
|
{
|
||||||
|
baseQuery = baseQuery.Where(b => b.TotalBacktests >= filter.TotalBacktestsMin.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.TotalBacktestsMax.HasValue)
|
||||||
|
{
|
||||||
|
baseQuery = baseQuery.Where(b => b.TotalBacktests <= filter.TotalBacktestsMax.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.CompletedBacktestsMin.HasValue)
|
||||||
|
{
|
||||||
|
baseQuery = baseQuery.Where(b => b.CompletedBacktests >= filter.CompletedBacktestsMin.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.CompletedBacktestsMax.HasValue)
|
||||||
|
{
|
||||||
|
baseQuery = baseQuery.Where(b => b.CompletedBacktests <= filter.CompletedBacktestsMax.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.ProgressPercentageMin.HasValue)
|
||||||
|
{
|
||||||
|
var minProgress = filter.ProgressPercentageMin.Value;
|
||||||
|
baseQuery = baseQuery.Where(b => b.TotalBacktests > 0 &&
|
||||||
|
(double)b.CompletedBacktests / b.TotalBacktests * 100 >= minProgress);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.ProgressPercentageMax.HasValue)
|
||||||
|
{
|
||||||
|
var maxProgress = filter.ProgressPercentageMax.Value;
|
||||||
|
baseQuery = baseQuery.Where(b => b.TotalBacktests > 0 &&
|
||||||
|
(double)b.CompletedBacktests / b.TotalBacktests * 100 <= maxProgress);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.CreatedAtFrom.HasValue)
|
||||||
|
{
|
||||||
|
baseQuery = baseQuery.Where(b => b.CreatedAt >= filter.CreatedAtFrom.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.CreatedAtTo.HasValue)
|
||||||
|
{
|
||||||
|
baseQuery = baseQuery.Where(b => b.CreatedAt <= filter.CreatedAtTo.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalCount = baseQuery.Count();
|
||||||
|
|
||||||
|
// Apply sorting
|
||||||
|
IQueryable<BundleBacktestRequestEntity> sortedQuery = sortBy switch
|
||||||
|
{
|
||||||
|
BundleBacktestRequestSortableColumn.RequestId => sortOrder == "desc"
|
||||||
|
? baseQuery.OrderByDescending(b => b.RequestId)
|
||||||
|
: baseQuery.OrderBy(b => b.RequestId),
|
||||||
|
BundleBacktestRequestSortableColumn.Name => sortOrder == "desc"
|
||||||
|
? baseQuery.OrderByDescending(b => b.Name)
|
||||||
|
: baseQuery.OrderBy(b => b.Name),
|
||||||
|
BundleBacktestRequestSortableColumn.Status => sortOrder == "desc"
|
||||||
|
? baseQuery.OrderByDescending(b => b.Status)
|
||||||
|
: baseQuery.OrderBy(b => b.Status),
|
||||||
|
BundleBacktestRequestSortableColumn.CreatedAt => sortOrder == "desc"
|
||||||
|
? baseQuery.OrderByDescending(b => b.CreatedAt)
|
||||||
|
: baseQuery.OrderBy(b => b.CreatedAt),
|
||||||
|
BundleBacktestRequestSortableColumn.CompletedAt => sortOrder == "desc"
|
||||||
|
? baseQuery.OrderByDescending(b => b.CompletedAt ?? DateTime.MinValue)
|
||||||
|
: baseQuery.OrderBy(b => b.CompletedAt ?? DateTime.MaxValue),
|
||||||
|
BundleBacktestRequestSortableColumn.TotalBacktests => sortOrder == "desc"
|
||||||
|
? baseQuery.OrderByDescending(b => b.TotalBacktests)
|
||||||
|
: baseQuery.OrderBy(b => b.TotalBacktests),
|
||||||
|
BundleBacktestRequestSortableColumn.CompletedBacktests => sortOrder == "desc"
|
||||||
|
? baseQuery.OrderByDescending(b => b.CompletedBacktests)
|
||||||
|
: baseQuery.OrderBy(b => b.CompletedBacktests),
|
||||||
|
BundleBacktestRequestSortableColumn.FailedBacktests => sortOrder == "desc"
|
||||||
|
? baseQuery.OrderByDescending(b => b.FailedBacktests)
|
||||||
|
: baseQuery.OrderBy(b => b.FailedBacktests),
|
||||||
|
BundleBacktestRequestSortableColumn.ProgressPercentage => sortOrder == "desc"
|
||||||
|
? baseQuery.OrderByDescending(b => b.TotalBacktests > 0 ? (double)b.CompletedBacktests / b.TotalBacktests : 0)
|
||||||
|
: baseQuery.OrderBy(b => b.TotalBacktests > 0 ? (double)b.CompletedBacktests / b.TotalBacktests : 0),
|
||||||
|
BundleBacktestRequestSortableColumn.UserId => sortOrder == "desc"
|
||||||
|
? baseQuery.OrderByDescending(b => b.UserId ?? int.MaxValue)
|
||||||
|
: baseQuery.OrderBy(b => b.UserId ?? int.MinValue),
|
||||||
|
BundleBacktestRequestSortableColumn.UserName => sortOrder == "desc"
|
||||||
|
? baseQuery.OrderByDescending(b => b.User != null ? b.User.Name : string.Empty)
|
||||||
|
: baseQuery.OrderBy(b => b.User != null ? b.User.Name : string.Empty),
|
||||||
|
BundleBacktestRequestSortableColumn.UpdatedAt => sortOrder == "desc"
|
||||||
|
? baseQuery.OrderByDescending(b => b.UpdatedAt)
|
||||||
|
: baseQuery.OrderBy(b => b.UpdatedAt),
|
||||||
|
_ => sortOrder == "desc"
|
||||||
|
? baseQuery.OrderByDescending(b => b.CreatedAt)
|
||||||
|
: baseQuery.OrderBy(b => b.CreatedAt)
|
||||||
|
};
|
||||||
|
|
||||||
|
var entities = sortedQuery
|
||||||
|
.Skip((page - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var mappedRequests = entities.Select(PostgreSqlMappers.Map);
|
||||||
|
|
||||||
|
return (mappedRequests, totalCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(IEnumerable<BundleBacktestRequest> BundleRequests, int TotalCount)> GetBundleBacktestRequestsPaginatedAsync(
|
||||||
|
int page,
|
||||||
|
int pageSize,
|
||||||
|
BundleBacktestRequestSortableColumn sortBy = BundleBacktestRequestSortableColumn.CreatedAt,
|
||||||
|
string sortOrder = "desc",
|
||||||
|
BundleBacktestRequestsFilter? filter = null)
|
||||||
|
{
|
||||||
|
var baseQuery = _context.BundleBacktestRequests
|
||||||
|
.AsNoTracking()
|
||||||
|
.Include(b => b.User)
|
||||||
|
.AsQueryable();
|
||||||
|
|
||||||
|
// Apply filters
|
||||||
|
if (filter != null)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(filter.NameContains))
|
||||||
|
{
|
||||||
|
var nameLike = $"%{filter.NameContains.Trim()}%";
|
||||||
|
baseQuery = baseQuery.Where(b => EF.Functions.ILike(b.Name, nameLike));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.Status.HasValue)
|
||||||
|
{
|
||||||
|
baseQuery = baseQuery.Where(b => b.Status == filter.Status.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.UserId.HasValue)
|
||||||
|
{
|
||||||
|
baseQuery = baseQuery.Where(b => b.UserId == filter.UserId.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(filter.UserNameContains))
|
||||||
|
{
|
||||||
|
var userNameLike = $"%{filter.UserNameContains.Trim()}%";
|
||||||
|
baseQuery = baseQuery.Where(b => b.User != null && EF.Functions.ILike(b.User.Name, userNameLike));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.TotalBacktestsMin.HasValue)
|
||||||
|
{
|
||||||
|
baseQuery = baseQuery.Where(b => b.TotalBacktests >= filter.TotalBacktestsMin.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.TotalBacktestsMax.HasValue)
|
||||||
|
{
|
||||||
|
baseQuery = baseQuery.Where(b => b.TotalBacktests <= filter.TotalBacktestsMax.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.CompletedBacktestsMin.HasValue)
|
||||||
|
{
|
||||||
|
baseQuery = baseQuery.Where(b => b.CompletedBacktests >= filter.CompletedBacktestsMin.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.CompletedBacktestsMax.HasValue)
|
||||||
|
{
|
||||||
|
baseQuery = baseQuery.Where(b => b.CompletedBacktests <= filter.CompletedBacktestsMax.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.ProgressPercentageMin.HasValue)
|
||||||
|
{
|
||||||
|
var minProgress = filter.ProgressPercentageMin.Value;
|
||||||
|
baseQuery = baseQuery.Where(b => b.TotalBacktests > 0 &&
|
||||||
|
(double)b.CompletedBacktests / b.TotalBacktests * 100 >= minProgress);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.ProgressPercentageMax.HasValue)
|
||||||
|
{
|
||||||
|
var maxProgress = filter.ProgressPercentageMax.Value;
|
||||||
|
baseQuery = baseQuery.Where(b => b.TotalBacktests > 0 &&
|
||||||
|
(double)b.CompletedBacktests / b.TotalBacktests * 100 <= maxProgress);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.CreatedAtFrom.HasValue)
|
||||||
|
{
|
||||||
|
baseQuery = baseQuery.Where(b => b.CreatedAt >= filter.CreatedAtFrom.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.CreatedAtTo.HasValue)
|
||||||
|
{
|
||||||
|
baseQuery = baseQuery.Where(b => b.CreatedAt <= filter.CreatedAtTo.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalCount = await baseQuery.CountAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
// Apply sorting
|
||||||
|
IQueryable<BundleBacktestRequestEntity> sortedQuery = sortBy switch
|
||||||
|
{
|
||||||
|
BundleBacktestRequestSortableColumn.RequestId => sortOrder == "desc"
|
||||||
|
? baseQuery.OrderByDescending(b => b.RequestId)
|
||||||
|
: baseQuery.OrderBy(b => b.RequestId),
|
||||||
|
BundleBacktestRequestSortableColumn.Name => sortOrder == "desc"
|
||||||
|
? baseQuery.OrderByDescending(b => b.Name)
|
||||||
|
: baseQuery.OrderBy(b => b.Name),
|
||||||
|
BundleBacktestRequestSortableColumn.Status => sortOrder == "desc"
|
||||||
|
? baseQuery.OrderByDescending(b => b.Status)
|
||||||
|
: baseQuery.OrderBy(b => b.Status),
|
||||||
|
BundleBacktestRequestSortableColumn.CreatedAt => sortOrder == "desc"
|
||||||
|
? baseQuery.OrderByDescending(b => b.CreatedAt)
|
||||||
|
: baseQuery.OrderBy(b => b.CreatedAt),
|
||||||
|
BundleBacktestRequestSortableColumn.CompletedAt => sortOrder == "desc"
|
||||||
|
? baseQuery.OrderByDescending(b => b.CompletedAt ?? DateTime.MinValue)
|
||||||
|
: baseQuery.OrderBy(b => b.CompletedAt ?? DateTime.MaxValue),
|
||||||
|
BundleBacktestRequestSortableColumn.TotalBacktests => sortOrder == "desc"
|
||||||
|
? baseQuery.OrderByDescending(b => b.TotalBacktests)
|
||||||
|
: baseQuery.OrderBy(b => b.TotalBacktests),
|
||||||
|
BundleBacktestRequestSortableColumn.CompletedBacktests => sortOrder == "desc"
|
||||||
|
? baseQuery.OrderByDescending(b => b.CompletedBacktests)
|
||||||
|
: baseQuery.OrderBy(b => b.CompletedBacktests),
|
||||||
|
BundleBacktestRequestSortableColumn.FailedBacktests => sortOrder == "desc"
|
||||||
|
? baseQuery.OrderByDescending(b => b.FailedBacktests)
|
||||||
|
: baseQuery.OrderBy(b => b.FailedBacktests),
|
||||||
|
BundleBacktestRequestSortableColumn.ProgressPercentage => sortOrder == "desc"
|
||||||
|
? baseQuery.OrderByDescending(b => b.TotalBacktests > 0 ? (double)b.CompletedBacktests / b.TotalBacktests : 0)
|
||||||
|
: baseQuery.OrderBy(b => b.TotalBacktests > 0 ? (double)b.CompletedBacktests / b.TotalBacktests : 0),
|
||||||
|
BundleBacktestRequestSortableColumn.UserId => sortOrder == "desc"
|
||||||
|
? baseQuery.OrderByDescending(b => b.UserId ?? int.MaxValue)
|
||||||
|
: baseQuery.OrderBy(b => b.UserId ?? int.MinValue),
|
||||||
|
BundleBacktestRequestSortableColumn.UserName => sortOrder == "desc"
|
||||||
|
? baseQuery.OrderByDescending(b => b.User != null ? b.User.Name : string.Empty)
|
||||||
|
: baseQuery.OrderBy(b => b.User != null ? b.User.Name : string.Empty),
|
||||||
|
BundleBacktestRequestSortableColumn.UpdatedAt => sortOrder == "desc"
|
||||||
|
? baseQuery.OrderByDescending(b => b.UpdatedAt)
|
||||||
|
: baseQuery.OrderBy(b => b.UpdatedAt),
|
||||||
|
_ => sortOrder == "desc"
|
||||||
|
? baseQuery.OrderByDescending(b => b.CreatedAt)
|
||||||
|
: baseQuery.OrderBy(b => b.CreatedAt)
|
||||||
|
};
|
||||||
|
|
||||||
|
var entities = await sortedQuery
|
||||||
|
.Skip((page - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
|
.ToListAsync()
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
var mappedRequests = entities.Select(PostgreSqlMappers.Map);
|
||||||
|
|
||||||
|
return (mappedRequests, totalCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<BundleBacktestRequestSummary> GetBundleBacktestRequestsSummaryAsync()
|
||||||
|
{
|
||||||
|
// Use ADO.NET directly for aggregation queries to avoid EF Core mapping issues
|
||||||
|
var connection = _context.Database.GetDbConnection();
|
||||||
|
await connection.OpenAsync();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var statusCounts = new List<BundleBacktestRequestStatusCountResult>();
|
||||||
|
var totalRequests = 0;
|
||||||
|
|
||||||
|
// Query 1: Status summary
|
||||||
|
// Note: Status is stored as text in PostgreSQL, not as integer
|
||||||
|
var statusSummarySql = @"
|
||||||
|
SELECT ""Status"", COUNT(*) as Count
|
||||||
|
FROM ""BundleBacktestRequests""
|
||||||
|
GROUP BY ""Status""
|
||||||
|
ORDER BY ""Status""";
|
||||||
|
|
||||||
|
using (var command = connection.CreateCommand())
|
||||||
|
{
|
||||||
|
command.CommandText = statusSummarySql;
|
||||||
|
using (var reader = await command.ExecuteReaderAsync())
|
||||||
|
{
|
||||||
|
while (await reader.ReadAsync())
|
||||||
|
{
|
||||||
|
var statusString = reader.GetString(0);
|
||||||
|
var count = reader.GetInt32(1);
|
||||||
|
|
||||||
|
// Parse the string status to enum
|
||||||
|
if (Enum.TryParse<BundleBacktestRequestStatus>(statusString, ignoreCase: true, out var status))
|
||||||
|
{
|
||||||
|
statusCounts.Add(new BundleBacktestRequestStatusCountResult
|
||||||
|
{
|
||||||
|
Status = status,
|
||||||
|
Count = count
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query 2: Total count
|
||||||
|
var totalCountSql = @"
|
||||||
|
SELECT COUNT(*) as Count
|
||||||
|
FROM ""BundleBacktestRequests""";
|
||||||
|
|
||||||
|
using (var command = connection.CreateCommand())
|
||||||
|
{
|
||||||
|
command.CommandText = totalCountSql;
|
||||||
|
var result = await command.ExecuteScalarAsync();
|
||||||
|
totalRequests = result != null ? Convert.ToInt32(result) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new BundleBacktestRequestSummary
|
||||||
|
{
|
||||||
|
StatusCounts = statusCounts.Select(s => new BundleBacktestRequestStatusCount
|
||||||
|
{
|
||||||
|
Status = s.Status,
|
||||||
|
Count = s.Count
|
||||||
|
}).ToList(),
|
||||||
|
TotalRequests = totalRequests
|
||||||
|
};
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await connection.CloseAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class BundleBacktestRequestStatusCountResult
|
||||||
|
{
|
||||||
|
public BundleBacktestRequestStatus Status { get; set; }
|
||||||
|
public int Count { get; set; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -382,6 +382,7 @@ public static class PostgreSqlMappers
|
|||||||
User = entity.User != null ? Map(entity.User) : null,
|
User = entity.User != null ? Map(entity.User) : null,
|
||||||
CreatedAt = entity.CreatedAt,
|
CreatedAt = entity.CreatedAt,
|
||||||
CompletedAt = entity.CompletedAt,
|
CompletedAt = entity.CompletedAt,
|
||||||
|
UpdatedAt = entity.UpdatedAt,
|
||||||
Status = entity.Status,
|
Status = entity.Status,
|
||||||
UniversalConfigJson = entity.UniversalConfigJson,
|
UniversalConfigJson = entity.UniversalConfigJson,
|
||||||
DateTimeRangesJson = entity.DateTimeRangesJson,
|
DateTimeRangesJson = entity.DateTimeRangesJson,
|
||||||
|
|||||||
@@ -368,6 +368,126 @@ export class AccountClient extends AuthorizedApiBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class AdminClient extends AuthorizedApiBase {
|
||||||
|
private http: { fetch(url: RequestInfo, init?: RequestInit): Promise<Response> };
|
||||||
|
private baseUrl: string;
|
||||||
|
protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined;
|
||||||
|
|
||||||
|
constructor(configuration: IConfig, baseUrl?: string, http?: { fetch(url: RequestInfo, init?: RequestInit): Promise<Response> }) {
|
||||||
|
super(configuration);
|
||||||
|
this.http = http ? http : window as any;
|
||||||
|
this.baseUrl = baseUrl ?? "http://localhost:5000";
|
||||||
|
}
|
||||||
|
|
||||||
|
admin_GetBundleBacktestRequestsPaginated(page: number | undefined, pageSize: number | undefined, sortBy: BundleBacktestRequestSortableColumn | undefined, sortOrder: string | null | undefined, nameContains: string | null | undefined, status: BundleBacktestRequestStatus | null | undefined, userId: number | null | undefined, userNameContains: string | null | undefined, totalBacktestsMin: number | null | undefined, totalBacktestsMax: number | null | undefined, completedBacktestsMin: number | null | undefined, completedBacktestsMax: number | null | undefined, progressPercentageMin: number | null | undefined, progressPercentageMax: number | null | undefined, createdAtFrom: Date | null | undefined, createdAtTo: Date | null | undefined): Promise<PaginatedBundleBacktestRequestsResponse> {
|
||||||
|
let url_ = this.baseUrl + "/Admin/BundleBacktestRequests/Paginated?";
|
||||||
|
if (page === null)
|
||||||
|
throw new Error("The parameter 'page' cannot be null.");
|
||||||
|
else if (page !== undefined)
|
||||||
|
url_ += "page=" + encodeURIComponent("" + page) + "&";
|
||||||
|
if (pageSize === null)
|
||||||
|
throw new Error("The parameter 'pageSize' cannot be null.");
|
||||||
|
else if (pageSize !== undefined)
|
||||||
|
url_ += "pageSize=" + encodeURIComponent("" + pageSize) + "&";
|
||||||
|
if (sortBy === null)
|
||||||
|
throw new Error("The parameter 'sortBy' cannot be null.");
|
||||||
|
else if (sortBy !== undefined)
|
||||||
|
url_ += "sortBy=" + encodeURIComponent("" + sortBy) + "&";
|
||||||
|
if (sortOrder !== undefined && sortOrder !== null)
|
||||||
|
url_ += "sortOrder=" + encodeURIComponent("" + sortOrder) + "&";
|
||||||
|
if (nameContains !== undefined && nameContains !== null)
|
||||||
|
url_ += "nameContains=" + encodeURIComponent("" + nameContains) + "&";
|
||||||
|
if (status !== undefined && status !== null)
|
||||||
|
url_ += "status=" + encodeURIComponent("" + status) + "&";
|
||||||
|
if (userId !== undefined && userId !== null)
|
||||||
|
url_ += "userId=" + encodeURIComponent("" + userId) + "&";
|
||||||
|
if (userNameContains !== undefined && userNameContains !== null)
|
||||||
|
url_ += "userNameContains=" + encodeURIComponent("" + userNameContains) + "&";
|
||||||
|
if (totalBacktestsMin !== undefined && totalBacktestsMin !== null)
|
||||||
|
url_ += "totalBacktestsMin=" + encodeURIComponent("" + totalBacktestsMin) + "&";
|
||||||
|
if (totalBacktestsMax !== undefined && totalBacktestsMax !== null)
|
||||||
|
url_ += "totalBacktestsMax=" + encodeURIComponent("" + totalBacktestsMax) + "&";
|
||||||
|
if (completedBacktestsMin !== undefined && completedBacktestsMin !== null)
|
||||||
|
url_ += "completedBacktestsMin=" + encodeURIComponent("" + completedBacktestsMin) + "&";
|
||||||
|
if (completedBacktestsMax !== undefined && completedBacktestsMax !== null)
|
||||||
|
url_ += "completedBacktestsMax=" + encodeURIComponent("" + completedBacktestsMax) + "&";
|
||||||
|
if (progressPercentageMin !== undefined && progressPercentageMin !== null)
|
||||||
|
url_ += "progressPercentageMin=" + encodeURIComponent("" + progressPercentageMin) + "&";
|
||||||
|
if (progressPercentageMax !== undefined && progressPercentageMax !== null)
|
||||||
|
url_ += "progressPercentageMax=" + encodeURIComponent("" + progressPercentageMax) + "&";
|
||||||
|
if (createdAtFrom !== undefined && createdAtFrom !== null)
|
||||||
|
url_ += "createdAtFrom=" + encodeURIComponent(createdAtFrom ? "" + createdAtFrom.toISOString() : "") + "&";
|
||||||
|
if (createdAtTo !== undefined && createdAtTo !== null)
|
||||||
|
url_ += "createdAtTo=" + encodeURIComponent(createdAtTo ? "" + createdAtTo.toISOString() : "") + "&";
|
||||||
|
url_ = url_.replace(/[?&]$/, "");
|
||||||
|
|
||||||
|
let options_: RequestInit = {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Accept": "application/json"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.transformOptions(options_).then(transformedOptions_ => {
|
||||||
|
return this.http.fetch(url_, transformedOptions_);
|
||||||
|
}).then((_response: Response) => {
|
||||||
|
return this.processAdmin_GetBundleBacktestRequestsPaginated(_response);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected processAdmin_GetBundleBacktestRequestsPaginated(response: Response): Promise<PaginatedBundleBacktestRequestsResponse> {
|
||||||
|
const status = response.status;
|
||||||
|
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
||||||
|
if (status === 200) {
|
||||||
|
return response.text().then((_responseText) => {
|
||||||
|
let result200: any = null;
|
||||||
|
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as PaginatedBundleBacktestRequestsResponse;
|
||||||
|
return result200;
|
||||||
|
});
|
||||||
|
} else if (status !== 200 && status !== 204) {
|
||||||
|
return response.text().then((_responseText) => {
|
||||||
|
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.resolve<PaginatedBundleBacktestRequestsResponse>(null as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
admin_GetBundleBacktestRequestsSummary(): Promise<BundleBacktestRequestSummaryResponse> {
|
||||||
|
let url_ = this.baseUrl + "/Admin/BundleBacktestRequests/Summary";
|
||||||
|
url_ = url_.replace(/[?&]$/, "");
|
||||||
|
|
||||||
|
let options_: RequestInit = {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Accept": "application/json"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.transformOptions(options_).then(transformedOptions_ => {
|
||||||
|
return this.http.fetch(url_, transformedOptions_);
|
||||||
|
}).then((_response: Response) => {
|
||||||
|
return this.processAdmin_GetBundleBacktestRequestsSummary(_response);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected processAdmin_GetBundleBacktestRequestsSummary(response: Response): Promise<BundleBacktestRequestSummaryResponse> {
|
||||||
|
const status = response.status;
|
||||||
|
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
||||||
|
if (status === 200) {
|
||||||
|
return response.text().then((_responseText) => {
|
||||||
|
let result200: any = null;
|
||||||
|
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as BundleBacktestRequestSummaryResponse;
|
||||||
|
return result200;
|
||||||
|
});
|
||||||
|
} else if (status !== 200 && status !== 204) {
|
||||||
|
return response.text().then((_responseText) => {
|
||||||
|
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.resolve<BundleBacktestRequestSummaryResponse>(null as any);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class BacktestClient extends AuthorizedApiBase {
|
export class BacktestClient extends AuthorizedApiBase {
|
||||||
private http: { fetch(url: RequestInfo, init?: RequestInit): Promise<Response> };
|
private http: { fetch(url: RequestInfo, init?: RequestInit): Promise<Response> };
|
||||||
private baseUrl: string;
|
private baseUrl: string;
|
||||||
@@ -4501,6 +4621,69 @@ export interface ExchangeInitializedStatus {
|
|||||||
isInitialized?: boolean;
|
isInitialized?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PaginatedBundleBacktestRequestsResponse {
|
||||||
|
bundleRequests?: BundleBacktestRequestListItemResponse[];
|
||||||
|
totalCount?: number;
|
||||||
|
currentPage?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
totalPages?: number;
|
||||||
|
hasNextPage?: boolean;
|
||||||
|
hasPreviousPage?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BundleBacktestRequestListItemResponse {
|
||||||
|
requestId?: string;
|
||||||
|
name?: string;
|
||||||
|
version?: number;
|
||||||
|
status?: string;
|
||||||
|
createdAt?: Date;
|
||||||
|
completedAt?: Date | null;
|
||||||
|
updatedAt?: Date;
|
||||||
|
totalBacktests?: number;
|
||||||
|
completedBacktests?: number;
|
||||||
|
failedBacktests?: number;
|
||||||
|
progressPercentage?: number;
|
||||||
|
userId?: number | null;
|
||||||
|
userName?: string | null;
|
||||||
|
errorMessage?: string | null;
|
||||||
|
currentBacktest?: string | null;
|
||||||
|
estimatedTimeRemainingSeconds?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum BundleBacktestRequestSortableColumn {
|
||||||
|
RequestId = "RequestId",
|
||||||
|
Name = "Name",
|
||||||
|
Status = "Status",
|
||||||
|
CreatedAt = "CreatedAt",
|
||||||
|
CompletedAt = "CompletedAt",
|
||||||
|
TotalBacktests = "TotalBacktests",
|
||||||
|
CompletedBacktests = "CompletedBacktests",
|
||||||
|
FailedBacktests = "FailedBacktests",
|
||||||
|
ProgressPercentage = "ProgressPercentage",
|
||||||
|
UserId = "UserId",
|
||||||
|
UserName = "UserName",
|
||||||
|
UpdatedAt = "UpdatedAt",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum BundleBacktestRequestStatus {
|
||||||
|
Pending = "Pending",
|
||||||
|
Running = "Running",
|
||||||
|
Completed = "Completed",
|
||||||
|
Failed = "Failed",
|
||||||
|
Cancelled = "Cancelled",
|
||||||
|
Saved = "Saved",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BundleBacktestRequestSummaryResponse {
|
||||||
|
statusSummary?: BundleBacktestRequestStatusSummary[];
|
||||||
|
totalRequests?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BundleBacktestRequestStatusSummary {
|
||||||
|
status?: string;
|
||||||
|
count?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Backtest {
|
export interface Backtest {
|
||||||
id: string;
|
id: string;
|
||||||
finalPnl: number;
|
finalPnl: number;
|
||||||
@@ -4937,15 +5120,7 @@ export interface BundleBacktestRequest {
|
|||||||
progressInfo?: string | null;
|
progressInfo?: string | null;
|
||||||
currentBacktest?: string | null;
|
currentBacktest?: string | null;
|
||||||
estimatedTimeRemainingSeconds?: number | null;
|
estimatedTimeRemainingSeconds?: number | null;
|
||||||
}
|
updatedAt: Date;
|
||||||
|
|
||||||
export enum BundleBacktestRequestStatus {
|
|
||||||
Pending = "Pending",
|
|
||||||
Running = "Running",
|
|
||||||
Completed = "Completed",
|
|
||||||
Failed = "Failed",
|
|
||||||
Cancelled = "Cancelled",
|
|
||||||
Saved = "Saved",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RunBundleBacktestRequest {
|
export interface RunBundleBacktestRequest {
|
||||||
|
|||||||
@@ -226,6 +226,69 @@ export interface ExchangeInitializedStatus {
|
|||||||
isInitialized?: boolean;
|
isInitialized?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PaginatedBundleBacktestRequestsResponse {
|
||||||
|
bundleRequests?: BundleBacktestRequestListItemResponse[];
|
||||||
|
totalCount?: number;
|
||||||
|
currentPage?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
totalPages?: number;
|
||||||
|
hasNextPage?: boolean;
|
||||||
|
hasPreviousPage?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BundleBacktestRequestListItemResponse {
|
||||||
|
requestId?: string;
|
||||||
|
name?: string;
|
||||||
|
version?: number;
|
||||||
|
status?: string;
|
||||||
|
createdAt?: Date;
|
||||||
|
completedAt?: Date | null;
|
||||||
|
updatedAt?: Date;
|
||||||
|
totalBacktests?: number;
|
||||||
|
completedBacktests?: number;
|
||||||
|
failedBacktests?: number;
|
||||||
|
progressPercentage?: number;
|
||||||
|
userId?: number | null;
|
||||||
|
userName?: string | null;
|
||||||
|
errorMessage?: string | null;
|
||||||
|
currentBacktest?: string | null;
|
||||||
|
estimatedTimeRemainingSeconds?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum BundleBacktestRequestSortableColumn {
|
||||||
|
RequestId = "RequestId",
|
||||||
|
Name = "Name",
|
||||||
|
Status = "Status",
|
||||||
|
CreatedAt = "CreatedAt",
|
||||||
|
CompletedAt = "CompletedAt",
|
||||||
|
TotalBacktests = "TotalBacktests",
|
||||||
|
CompletedBacktests = "CompletedBacktests",
|
||||||
|
FailedBacktests = "FailedBacktests",
|
||||||
|
ProgressPercentage = "ProgressPercentage",
|
||||||
|
UserId = "UserId",
|
||||||
|
UserName = "UserName",
|
||||||
|
UpdatedAt = "UpdatedAt",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum BundleBacktestRequestStatus {
|
||||||
|
Pending = "Pending",
|
||||||
|
Running = "Running",
|
||||||
|
Completed = "Completed",
|
||||||
|
Failed = "Failed",
|
||||||
|
Cancelled = "Cancelled",
|
||||||
|
Saved = "Saved",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BundleBacktestRequestSummaryResponse {
|
||||||
|
statusSummary?: BundleBacktestRequestStatusSummary[];
|
||||||
|
totalRequests?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BundleBacktestRequestStatusSummary {
|
||||||
|
status?: string;
|
||||||
|
count?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Backtest {
|
export interface Backtest {
|
||||||
id: string;
|
id: string;
|
||||||
finalPnl: number;
|
finalPnl: number;
|
||||||
@@ -662,15 +725,7 @@ export interface BundleBacktestRequest {
|
|||||||
progressInfo?: string | null;
|
progressInfo?: string | null;
|
||||||
currentBacktest?: string | null;
|
currentBacktest?: string | null;
|
||||||
estimatedTimeRemainingSeconds?: number | null;
|
estimatedTimeRemainingSeconds?: number | null;
|
||||||
}
|
updatedAt: Date;
|
||||||
|
|
||||||
export enum BundleBacktestRequestStatus {
|
|
||||||
Pending = "Pending",
|
|
||||||
Running = "Running",
|
|
||||||
Completed = "Completed",
|
|
||||||
Failed = "Failed",
|
|
||||||
Cancelled = "Cancelled",
|
|
||||||
Saved = "Saved",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RunBundleBacktestRequest {
|
export interface RunBundleBacktestRequest {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {Tabs} from '../../components/mollecules'
|
|||||||
import AccountSettings from './account/accountSettings'
|
import AccountSettings from './account/accountSettings'
|
||||||
import WhitelistSettings from './whitelist/whitelistSettings'
|
import WhitelistSettings from './whitelist/whitelistSettings'
|
||||||
import JobsSettings from './jobs/jobsSettings'
|
import JobsSettings from './jobs/jobsSettings'
|
||||||
|
import BundleBacktestRequestsSettings from './bundleBacktestRequests/bundleBacktestRequestsSettings'
|
||||||
|
|
||||||
type TabsType = {
|
type TabsType = {
|
||||||
label: string
|
label: string
|
||||||
@@ -29,6 +30,11 @@ const tabs: TabsType = [
|
|||||||
index: 3,
|
index: 3,
|
||||||
label: 'Jobs',
|
label: 'Jobs',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Component: BundleBacktestRequestsSettings,
|
||||||
|
index: 4,
|
||||||
|
label: 'Bundle',
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const Admin: React.FC = () => {
|
const Admin: React.FC = () => {
|
||||||
|
|||||||
@@ -0,0 +1,534 @@
|
|||||||
|
import {useState} from 'react'
|
||||||
|
import {useQuery} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import useApiUrlStore from '../../../app/store/apiStore'
|
||||||
|
import {
|
||||||
|
AdminClient,
|
||||||
|
BundleBacktestRequestSortableColumn,
|
||||||
|
BundleBacktestRequestStatus
|
||||||
|
} from '../../../generated/ManagingApi'
|
||||||
|
|
||||||
|
import BundleBacktestRequestsTable from './bundleBacktestRequestsTable'
|
||||||
|
|
||||||
|
const BundleBacktestRequestsSettings: React.FC = () => {
|
||||||
|
const { apiUrl } = useApiUrlStore()
|
||||||
|
const [page, setPage] = useState(1)
|
||||||
|
const [pageSize, setPageSize] = useState(50)
|
||||||
|
const [sortBy, setSortBy] = useState<BundleBacktestRequestSortableColumn>(BundleBacktestRequestSortableColumn.CreatedAt)
|
||||||
|
const [sortOrder, setSortOrder] = useState<string>('desc')
|
||||||
|
const [nameContains, setNameContains] = useState<string>('')
|
||||||
|
const [statusFilter, setStatusFilter] = useState<BundleBacktestRequestStatus | null>(BundleBacktestRequestStatus.Failed)
|
||||||
|
const [userIdFilter, setUserIdFilter] = useState<string>('')
|
||||||
|
const [userNameContains, setUserNameContains] = useState<string>('')
|
||||||
|
const [totalBacktestsMin, setTotalBacktestsMin] = useState<string>('')
|
||||||
|
const [totalBacktestsMax, setTotalBacktestsMax] = useState<string>('')
|
||||||
|
const [completedBacktestsMin, setCompletedBacktestsMin] = useState<string>('')
|
||||||
|
const [completedBacktestsMax, setCompletedBacktestsMax] = useState<string>('')
|
||||||
|
const [progressPercentageMin, setProgressPercentageMin] = useState<string>('')
|
||||||
|
const [progressPercentageMax, setProgressPercentageMax] = useState<string>('')
|
||||||
|
const [filtersOpen, setFiltersOpen] = useState<boolean>(false)
|
||||||
|
const [showTable, setShowTable] = useState<boolean>(true)
|
||||||
|
|
||||||
|
const adminClient = new AdminClient({}, apiUrl)
|
||||||
|
|
||||||
|
// Fetch bundle backtest requests summary statistics
|
||||||
|
const {
|
||||||
|
data: bundleSummary,
|
||||||
|
isLoading: isLoadingSummary
|
||||||
|
} = useQuery({
|
||||||
|
queryKey: ['bundleBacktestRequestsSummary'],
|
||||||
|
queryFn: async () => {
|
||||||
|
return await adminClient.admin_GetBundleBacktestRequestsSummary()
|
||||||
|
},
|
||||||
|
staleTime: 10000, // 10 seconds
|
||||||
|
gcTime: 5 * 60 * 1000,
|
||||||
|
refetchInterval: 5000, // Auto-refresh every 5 seconds
|
||||||
|
})
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: bundleRequestsData,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
refetch
|
||||||
|
} = useQuery({
|
||||||
|
queryKey: ['bundleBacktestRequests', page, pageSize, sortBy, sortOrder, nameContains, statusFilter, userIdFilter, userNameContains, totalBacktestsMin, totalBacktestsMax, completedBacktestsMin, completedBacktestsMax, progressPercentageMin, progressPercentageMax],
|
||||||
|
queryFn: async () => {
|
||||||
|
return await adminClient.admin_GetBundleBacktestRequestsPaginated(
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
sortBy,
|
||||||
|
sortOrder,
|
||||||
|
nameContains || null,
|
||||||
|
statusFilter || null,
|
||||||
|
userIdFilter ? parseInt(userIdFilter) : null,
|
||||||
|
userNameContains || null,
|
||||||
|
totalBacktestsMin ? parseInt(totalBacktestsMin) : null,
|
||||||
|
totalBacktestsMax ? parseInt(totalBacktestsMax) : null,
|
||||||
|
completedBacktestsMin ? parseInt(completedBacktestsMin) : null,
|
||||||
|
completedBacktestsMax ? parseInt(completedBacktestsMax) : null,
|
||||||
|
progressPercentageMin ? parseFloat(progressPercentageMin) : null,
|
||||||
|
progressPercentageMax ? parseFloat(progressPercentageMax) : null,
|
||||||
|
null, // createdAtFrom
|
||||||
|
null // createdAtTo
|
||||||
|
)
|
||||||
|
},
|
||||||
|
enabled: showTable,
|
||||||
|
staleTime: 10000, // 10 seconds
|
||||||
|
gcTime: 5 * 60 * 1000,
|
||||||
|
refetchInterval: showTable ? 5000 : false, // Auto-refresh every 5 seconds when table is shown
|
||||||
|
})
|
||||||
|
|
||||||
|
const bundleRequests = bundleRequestsData?.bundleRequests || []
|
||||||
|
const totalCount = bundleRequestsData?.totalCount || 0
|
||||||
|
const totalPages = bundleRequestsData?.totalPages || 0
|
||||||
|
const currentPage = bundleRequestsData?.currentPage || 1
|
||||||
|
|
||||||
|
const handlePageChange = (newPage: number) => {
|
||||||
|
setPage(newPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSortChange = (newSortBy: BundleBacktestRequestSortableColumn) => {
|
||||||
|
if (sortBy === newSortBy) {
|
||||||
|
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')
|
||||||
|
} else {
|
||||||
|
setSortBy(newSortBy)
|
||||||
|
setSortOrder('desc')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFilterChange = () => {
|
||||||
|
setPage(1) // Reset to first page when filters change
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearFilters = () => {
|
||||||
|
setNameContains('')
|
||||||
|
setStatusFilter(null)
|
||||||
|
setUserIdFilter('')
|
||||||
|
setUserNameContains('')
|
||||||
|
setTotalBacktestsMin('')
|
||||||
|
setTotalBacktestsMax('')
|
||||||
|
setCompletedBacktestsMin('')
|
||||||
|
setCompletedBacktestsMax('')
|
||||||
|
setProgressPercentageMin('')
|
||||||
|
setProgressPercentageMax('')
|
||||||
|
setPage(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to get status badge color
|
||||||
|
const getStatusBadgeColor = (status: string | undefined) => {
|
||||||
|
if (!status) return 'badge-ghost'
|
||||||
|
const statusLower = status.toLowerCase()
|
||||||
|
switch (statusLower) {
|
||||||
|
case 'pending':
|
||||||
|
return 'badge-warning'
|
||||||
|
case 'running':
|
||||||
|
return 'badge-info'
|
||||||
|
case 'completed':
|
||||||
|
return 'badge-success'
|
||||||
|
case 'failed':
|
||||||
|
return 'badge-error'
|
||||||
|
case 'saved':
|
||||||
|
return 'badge-ghost'
|
||||||
|
case 'cancelled':
|
||||||
|
return 'badge-ghost'
|
||||||
|
default:
|
||||||
|
return 'badge-ghost'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to get status info (icon, description, color)
|
||||||
|
const getStatusInfo = (status: string) => {
|
||||||
|
const statusLower = status.toLowerCase()
|
||||||
|
let statusIcon, statusDesc, statusColor
|
||||||
|
|
||||||
|
switch (statusLower) {
|
||||||
|
case 'pending':
|
||||||
|
statusIcon = (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" className="w-6 h-6">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
statusDesc = 'Waiting to be processed'
|
||||||
|
statusColor = 'text-warning'
|
||||||
|
break
|
||||||
|
case 'running':
|
||||||
|
statusIcon = (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" className="w-6 h-6">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.348a1.125 1.125 0 010 1.971l-11.54 6.347a1.125 1.125 0 01-1.667-.986V5.653z" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
statusDesc = 'Currently processing'
|
||||||
|
statusColor = 'text-info'
|
||||||
|
break
|
||||||
|
case 'completed':
|
||||||
|
statusIcon = (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" className="w-6 h-6">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
statusDesc = 'Successfully finished'
|
||||||
|
statusColor = 'text-success'
|
||||||
|
break
|
||||||
|
case 'failed':
|
||||||
|
statusIcon = (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" className="w-6 h-6">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
statusDesc = 'Requires attention'
|
||||||
|
statusColor = 'text-error'
|
||||||
|
break
|
||||||
|
case 'saved':
|
||||||
|
statusIcon = (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" className="w-6 h-6">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M17.593 3.322c1.1.128 1.907 1.077 1.907 2.185V21L12 17.25 4.5 21V5.507c0-1.108.806-2.057 1.907-2.185a48.507 48.507 0 0111.186 0z" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
statusDesc = 'Saved as template'
|
||||||
|
statusColor = 'text-neutral'
|
||||||
|
break
|
||||||
|
case 'cancelled':
|
||||||
|
statusIcon = (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" className="w-6 h-6">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
statusDesc = 'Cancelled by user'
|
||||||
|
statusColor = 'text-neutral'
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
statusIcon = (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" className="w-6 h-6">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9 5.25h.008v.008H12v-.008z" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
statusDesc = 'Unknown status'
|
||||||
|
statusColor = 'text-base-content'
|
||||||
|
}
|
||||||
|
|
||||||
|
return { statusIcon, statusDesc, statusColor }
|
||||||
|
}
|
||||||
|
|
||||||
|
// All possible statuses for skeleton display
|
||||||
|
const allStatuses = ['Pending', 'Running', 'Completed', 'Failed', 'Saved', 'Cancelled']
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-4 pb-20">
|
||||||
|
{/* Bundle Backtest Requests Summary Statistics */}
|
||||||
|
<div className="mb-8">
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Status Overview Section */}
|
||||||
|
<div className="card bg-base-100 shadow-md">
|
||||||
|
<div className="card-body">
|
||||||
|
<h3 className="card-title text-xl mb-4">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" className="w-6 h-6">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 00-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75 2.25 2.25 0 00-.1-.664m-5.8 0A2.251 2.251 0 0113.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25zM6.75 12h.008v.008H6.75V12zm0 3h.008v.008H6.75V15zm0 3h.008v.008H6.75V18z" />
|
||||||
|
</svg>
|
||||||
|
Status Overview
|
||||||
|
{isLoadingSummary && (
|
||||||
|
<span className="loading loading-spinner loading-sm ml-2"></span>
|
||||||
|
)}
|
||||||
|
</h3>
|
||||||
|
<div className="grid grid-cols-3 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4">
|
||||||
|
{isLoadingSummary ? (
|
||||||
|
// Show skeleton with all statuses set to 0
|
||||||
|
<>
|
||||||
|
{allStatuses.map((status) => {
|
||||||
|
const { statusIcon, statusDesc, statusColor } = getStatusInfo(status)
|
||||||
|
return (
|
||||||
|
<div key={status} className="card bg-base-200 shadow-sm">
|
||||||
|
<div className="card-body p-4">
|
||||||
|
<div className="stat p-0">
|
||||||
|
<div className={`stat-figure ${statusColor} opacity-80 hidden md:block`}>
|
||||||
|
{statusIcon}
|
||||||
|
</div>
|
||||||
|
<div className="stat-title">{status}</div>
|
||||||
|
<div className={`stat-value ${statusColor} text-2xl`}>0</div>
|
||||||
|
<div className="stat-desc text-xs hidden md:block">{statusDesc}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
{/* Total card in skeleton */}
|
||||||
|
<div className="card bg-base-200 shadow-sm">
|
||||||
|
<div className="card-body p-4">
|
||||||
|
<div className="stat p-0">
|
||||||
|
<div className="stat-figure text-primary opacity-80 hidden md:block">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" className="w-6 h-6">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 3v11.25A2.25 2.25 0 006 16.5h2.25M3.75 3h-1.5m1.5 0h16.5m0 0h1.5m-1.5 0v11.25A2.25 2.25 0 0118 16.5h-2.25m-7.5 0h7.5m-7.5 0l-1 3m8.5-3l1 3m0 0l.5 1.5m-.5-1.5h-9.5m0 0l-.5 1.5M9 11.25v1.5M12 9v3.75m3-3.75v3.75m-3 .75h.008v.008H12v-.008z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="stat-title">Total</div>
|
||||||
|
<div className="stat-value text-primary text-2xl">0</div>
|
||||||
|
<div className="stat-desc text-xs hidden md:block">Across all statuses</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : bundleSummary?.statusSummary && bundleSummary.statusSummary.length > 0 ? (
|
||||||
|
// Show actual data
|
||||||
|
<>
|
||||||
|
{bundleSummary.statusSummary.map((statusItem) => {
|
||||||
|
const { statusIcon, statusDesc, statusColor } = getStatusInfo(statusItem.status || '')
|
||||||
|
return (
|
||||||
|
<div key={statusItem.status} className="card bg-base-200 shadow-sm">
|
||||||
|
<div className="card-body p-4">
|
||||||
|
<div className="stat p-0">
|
||||||
|
<div className={`stat-figure ${statusColor} opacity-80 hidden md:block`}>
|
||||||
|
{statusIcon}
|
||||||
|
</div>
|
||||||
|
<div className="stat-title">{statusItem.status || 'Unknown'}</div>
|
||||||
|
<div className={`stat-value ${statusColor} text-2xl`}>{statusItem.count || 0}</div>
|
||||||
|
<div className="stat-desc text-xs hidden md:block">{statusDesc}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
{/* Total card with actual data */}
|
||||||
|
<div className="card bg-base-200 shadow-sm">
|
||||||
|
<div className="card-body p-4">
|
||||||
|
<div className="stat p-0">
|
||||||
|
<div className="stat-figure text-primary opacity-80 hidden md:block">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" className="w-6 h-6">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 3v11.25A2.25 2.25 0 006 16.5h2.25M3.75 3h-1.5m1.5 0h16.5m0 0h1.5m-1.5 0v11.25A2.25 2.25 0 0118 16.5h-2.25m-7.5 0h7.5m-7.5 0l-1 3m8.5-3l1 3m0 0l.5 1.5m-.5-1.5h-9.5m0 0l-.5 1.5M9 11.25v1.5M12 9v3.75m3-3.75v3.75m-3 .75h.008v.008H12v-.008z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="stat-title">Total</div>
|
||||||
|
<div className="stat-value text-primary text-2xl">{bundleSummary?.totalRequests || 0}</div>
|
||||||
|
<div className="stat-desc text-xs hidden md:block">Across all statuses</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filters Section */}
|
||||||
|
<div className="card bg-base-100 shadow-md mb-4">
|
||||||
|
<div className="card-body py-4">
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<h3 className="card-title text-lg">Filters</h3>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
className="btn btn-sm btn-ghost"
|
||||||
|
onClick={() => setFiltersOpen(!filtersOpen)}
|
||||||
|
>
|
||||||
|
{filtersOpen ? 'Hide' : 'Show'} Filters
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-sm btn-outline"
|
||||||
|
onClick={clearFilters}
|
||||||
|
>
|
||||||
|
Clear Filters
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{filtersOpen && (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">Name Contains</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search by name..."
|
||||||
|
className="input input-bordered input-sm"
|
||||||
|
value={nameContains}
|
||||||
|
onChange={(e) => {
|
||||||
|
setNameContains(e.target.value)
|
||||||
|
handleFilterChange()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">Status</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
className="select select-bordered select-sm"
|
||||||
|
value={statusFilter || ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
setStatusFilter(e.target.value ? (e.target.value as BundleBacktestRequestStatus) : null)
|
||||||
|
handleFilterChange()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="">All</option>
|
||||||
|
<option value={BundleBacktestRequestStatus.Pending}>Pending</option>
|
||||||
|
<option value={BundleBacktestRequestStatus.Running}>Running</option>
|
||||||
|
<option value={BundleBacktestRequestStatus.Completed}>Completed</option>
|
||||||
|
<option value={BundleBacktestRequestStatus.Failed}>Failed</option>
|
||||||
|
<option value={BundleBacktestRequestStatus.Saved}>Saved</option>
|
||||||
|
<option value={BundleBacktestRequestStatus.Cancelled}>Cancelled</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">User ID</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
placeholder="Filter by user ID..."
|
||||||
|
className="input input-bordered input-sm"
|
||||||
|
value={userIdFilter}
|
||||||
|
onChange={(e) => {
|
||||||
|
setUserIdFilter(e.target.value)
|
||||||
|
handleFilterChange()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">User Name Contains</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search by user name..."
|
||||||
|
className="input input-bordered input-sm"
|
||||||
|
value={userNameContains}
|
||||||
|
onChange={(e) => {
|
||||||
|
setUserNameContains(e.target.value)
|
||||||
|
handleFilterChange()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">Total Backtests Min</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
placeholder="Min total backtests..."
|
||||||
|
className="input input-bordered input-sm"
|
||||||
|
value={totalBacktestsMin}
|
||||||
|
onChange={(e) => {
|
||||||
|
setTotalBacktestsMin(e.target.value)
|
||||||
|
handleFilterChange()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">Total Backtests Max</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
placeholder="Max total backtests..."
|
||||||
|
className="input input-bordered input-sm"
|
||||||
|
value={totalBacktestsMax}
|
||||||
|
onChange={(e) => {
|
||||||
|
setTotalBacktestsMax(e.target.value)
|
||||||
|
handleFilterChange()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">Completed Backtests Min</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
placeholder="Min completed..."
|
||||||
|
className="input input-bordered input-sm"
|
||||||
|
value={completedBacktestsMin}
|
||||||
|
onChange={(e) => {
|
||||||
|
setCompletedBacktestsMin(e.target.value)
|
||||||
|
handleFilterChange()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">Completed Backtests Max</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
placeholder="Max completed..."
|
||||||
|
className="input input-bordered input-sm"
|
||||||
|
value={completedBacktestsMax}
|
||||||
|
onChange={(e) => {
|
||||||
|
setCompletedBacktestsMax(e.target.value)
|
||||||
|
handleFilterChange()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">Progress % Min (0-100)</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
step="0.1"
|
||||||
|
placeholder="Min progress %..."
|
||||||
|
className="input input-bordered input-sm"
|
||||||
|
value={progressPercentageMin}
|
||||||
|
onChange={(e) => {
|
||||||
|
setProgressPercentageMin(e.target.value)
|
||||||
|
handleFilterChange()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">Progress % Max (0-100)</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
step="0.1"
|
||||||
|
placeholder="Max progress %..."
|
||||||
|
className="input input-bordered input-sm"
|
||||||
|
value={progressPercentageMax}
|
||||||
|
onChange={(e) => {
|
||||||
|
setProgressPercentageMax(e.target.value)
|
||||||
|
handleFilterChange()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<BundleBacktestRequestsTable
|
||||||
|
bundleRequests={bundleRequests}
|
||||||
|
isLoading={isLoading}
|
||||||
|
totalCount={totalCount}
|
||||||
|
currentPage={currentPage}
|
||||||
|
totalPages={totalPages}
|
||||||
|
pageSize={pageSize}
|
||||||
|
sortBy={sortBy}
|
||||||
|
sortOrder={sortOrder}
|
||||||
|
onPageChange={handlePageChange}
|
||||||
|
onSortChange={handleSortChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="alert alert-error mt-4">
|
||||||
|
<span>Failed to load bundle backtest requests. Please try again.</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BundleBacktestRequestsSettings
|
||||||
|
|
||||||
@@ -0,0 +1,292 @@
|
|||||||
|
import React, {useMemo} from 'react'
|
||||||
|
import {
|
||||||
|
type BundleBacktestRequestListItemResponse,
|
||||||
|
BundleBacktestRequestSortableColumn
|
||||||
|
} from '../../../generated/ManagingApi'
|
||||||
|
import {Table} from '../../../components/mollecules'
|
||||||
|
|
||||||
|
interface IBundleBacktestRequestsTable {
|
||||||
|
bundleRequests: BundleBacktestRequestListItemResponse[]
|
||||||
|
isLoading: boolean
|
||||||
|
totalCount: number
|
||||||
|
currentPage: number
|
||||||
|
totalPages: number
|
||||||
|
pageSize: number
|
||||||
|
sortBy: BundleBacktestRequestSortableColumn
|
||||||
|
sortOrder: string
|
||||||
|
onPageChange: (page: number) => void
|
||||||
|
onSortChange: (sortBy: BundleBacktestRequestSortableColumn) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const BundleBacktestRequestsTable: React.FC<IBundleBacktestRequestsTable> = ({
|
||||||
|
bundleRequests,
|
||||||
|
isLoading,
|
||||||
|
totalCount,
|
||||||
|
currentPage,
|
||||||
|
totalPages,
|
||||||
|
pageSize,
|
||||||
|
sortBy,
|
||||||
|
sortOrder,
|
||||||
|
onPageChange,
|
||||||
|
onSortChange
|
||||||
|
}) => {
|
||||||
|
const getStatusBadge = (status: string | null | undefined) => {
|
||||||
|
if (!status) return <span className="badge badge-sm">-</span>
|
||||||
|
|
||||||
|
const statusLower = status.toLowerCase()
|
||||||
|
switch (statusLower) {
|
||||||
|
case 'pending':
|
||||||
|
return <span className="badge badge-sm badge-warning">Pending</span>
|
||||||
|
case 'running':
|
||||||
|
return <span className="badge badge-sm badge-info">Running</span>
|
||||||
|
case 'completed':
|
||||||
|
return <span className="badge badge-sm badge-success">Completed</span>
|
||||||
|
case 'failed':
|
||||||
|
return <span className="badge badge-sm badge-error">Failed</span>
|
||||||
|
case 'saved':
|
||||||
|
return <span className="badge badge-sm badge-ghost">Saved</span>
|
||||||
|
case 'cancelled':
|
||||||
|
return <span className="badge badge-sm badge-ghost">Cancelled</span>
|
||||||
|
default:
|
||||||
|
return <span className="badge badge-sm">{status}</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDate = (date: Date | string | null | undefined) => {
|
||||||
|
if (!date) return '-'
|
||||||
|
try {
|
||||||
|
return new Date(date).toLocaleString()
|
||||||
|
} catch {
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatProgress = (progress: number | undefined) => {
|
||||||
|
if (progress === undefined || progress === null) return '-'
|
||||||
|
return `${progress.toFixed(1)}%`
|
||||||
|
}
|
||||||
|
|
||||||
|
const SortableHeader = ({ column, label }: { column: BundleBacktestRequestSortableColumn; label: string }) => {
|
||||||
|
const isActive = sortBy === column
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-1 cursor-pointer hover:text-primary text-base-content"
|
||||||
|
onClick={() => onSortChange(column)}
|
||||||
|
>
|
||||||
|
<span className="font-semibold">{label}</span>
|
||||||
|
{isActive && (
|
||||||
|
<span className="text-xs">
|
||||||
|
{sortOrder === 'asc' ? '↑' : '↓'}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = useMemo(() => [
|
||||||
|
{
|
||||||
|
id: 'name',
|
||||||
|
Header: () => <SortableHeader column={BundleBacktestRequestSortableColumn.Name} label="Name" />,
|
||||||
|
accessor: (row: BundleBacktestRequestListItemResponse) => (
|
||||||
|
<div>
|
||||||
|
<div className="font-semibold">{row.name || '-'}</div>
|
||||||
|
{row.version && <div className="text-xs text-gray-500">v{row.version}</div>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'status',
|
||||||
|
Header: () => <SortableHeader column={BundleBacktestRequestSortableColumn.Status} label="Status" />,
|
||||||
|
accessor: (row: BundleBacktestRequestListItemResponse) => getStatusBadge(row.status)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'user',
|
||||||
|
Header: () => <SortableHeader column={BundleBacktestRequestSortableColumn.UserName} label="User" />,
|
||||||
|
accessor: (row: BundleBacktestRequestListItemResponse) => (
|
||||||
|
<div>
|
||||||
|
{row.userName ? (
|
||||||
|
<>
|
||||||
|
<div className="font-semibold">{row.userName}</div>
|
||||||
|
{row.userId && <div className="text-xs text-gray-500">ID: {row.userId}</div>}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span className="text-gray-400">-</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'progress',
|
||||||
|
Header: () => <SortableHeader column={BundleBacktestRequestSortableColumn.ProgressPercentage} label="Progress" />,
|
||||||
|
accessor: (row: BundleBacktestRequestListItemResponse) => (
|
||||||
|
<div>
|
||||||
|
<div className="font-semibold">{formatProgress(row.progressPercentage)}</div>
|
||||||
|
<div className="text-xs text-gray-500">
|
||||||
|
{row.completedBacktests || 0} / {row.totalBacktests || 0}
|
||||||
|
</div>
|
||||||
|
{(row.totalBacktests || 0) > 0 && (
|
||||||
|
<progress
|
||||||
|
className="progress progress-primary w-full h-2 mt-1"
|
||||||
|
value={row.progressPercentage || 0}
|
||||||
|
max={100}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'backtests',
|
||||||
|
Header: () => <SortableHeader column={BundleBacktestRequestSortableColumn.TotalBacktests} label="Backtests" />,
|
||||||
|
accessor: (row: BundleBacktestRequestListItemResponse) => (
|
||||||
|
<div>
|
||||||
|
<div>Total: {row.totalBacktests || 0}</div>
|
||||||
|
<div className="text-xs text-gray-500">
|
||||||
|
Completed: {row.completedBacktests || 0} | Failed: {row.failedBacktests || 0}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'dates',
|
||||||
|
Header: () => <SortableHeader column={BundleBacktestRequestSortableColumn.CreatedAt} label="Dates" />,
|
||||||
|
accessor: (row: BundleBacktestRequestListItemResponse) => (
|
||||||
|
<div className="text-xs">
|
||||||
|
<div>Created: {formatDate(row.createdAt)}</div>
|
||||||
|
{row.completedAt && (
|
||||||
|
<div className="text-gray-500">Completed: {formatDate(row.completedAt)}</div>
|
||||||
|
)}
|
||||||
|
{row.updatedAt && (
|
||||||
|
<div className="text-gray-400">Updated: {formatDate(row.updatedAt)}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'error',
|
||||||
|
Header: 'Error',
|
||||||
|
accessor: (row: BundleBacktestRequestListItemResponse) => (
|
||||||
|
row.errorMessage ? (
|
||||||
|
<div className="tooltip tooltip-left" data-tip={row.errorMessage}>
|
||||||
|
<span className="badge badge-sm badge-error">Error</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span className="text-gray-400">-</span>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'requestId',
|
||||||
|
Header: () => <SortableHeader column={BundleBacktestRequestSortableColumn.RequestId} label="Request ID" />,
|
||||||
|
accessor: (row: BundleBacktestRequestListItemResponse) => (
|
||||||
|
<span className="font-mono text-xs">{row.requestId?.substring(0, 8)}...</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
], [sortBy, sortOrder, onSortChange])
|
||||||
|
|
||||||
|
const tableData = useMemo(() => {
|
||||||
|
return bundleRequests.map((request) => ({
|
||||||
|
...request,
|
||||||
|
key: request.requestId
|
||||||
|
}))
|
||||||
|
}, [bundleRequests])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{isLoading && (
|
||||||
|
<div className="flex justify-center my-4">
|
||||||
|
<span className="loading loading-spinner loading-lg"></span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLoading && bundleRequests.length === 0 && (
|
||||||
|
<div className="alert alert-info">
|
||||||
|
<span>No bundle backtest requests found.</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLoading && bundleRequests.length > 0 && (
|
||||||
|
<>
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
data={tableData}
|
||||||
|
showPagination={false}
|
||||||
|
hiddenColumns={[]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pagination Info and Controls */}
|
||||||
|
<div className="mt-4 flex flex-col items-center gap-2">
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
Total requests: {totalCount} | Page {currentPage} of {totalPages}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Manual Pagination */}
|
||||||
|
{totalPages > 1 && (
|
||||||
|
<div className="flex justify-center items-center gap-2">
|
||||||
|
<button
|
||||||
|
className="btn btn-sm"
|
||||||
|
onClick={() => onPageChange(1)}
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
>
|
||||||
|
{'<<'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-sm"
|
||||||
|
onClick={() => onPageChange(currentPage - 1)}
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
>
|
||||||
|
{'<'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Page numbers */}
|
||||||
|
<div className="flex gap-1">
|
||||||
|
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
|
||||||
|
let pageNum
|
||||||
|
if (totalPages <= 5) {
|
||||||
|
pageNum = i + 1
|
||||||
|
} else if (currentPage <= 3) {
|
||||||
|
pageNum = i + 1
|
||||||
|
} else if (currentPage >= totalPages - 2) {
|
||||||
|
pageNum = totalPages - 4 + i
|
||||||
|
} else {
|
||||||
|
pageNum = currentPage - 2 + i
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={pageNum}
|
||||||
|
className={`btn btn-sm ${currentPage === pageNum ? 'btn-primary' : ''}`}
|
||||||
|
onClick={() => onPageChange(pageNum)}
|
||||||
|
>
|
||||||
|
{pageNum}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="btn btn-sm"
|
||||||
|
onClick={() => onPageChange(currentPage + 1)}
|
||||||
|
disabled={currentPage >= totalPages}
|
||||||
|
>
|
||||||
|
{'>'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-sm"
|
||||||
|
onClick={() => onPageChange(totalPages)}
|
||||||
|
disabled={currentPage >= totalPages}
|
||||||
|
>
|
||||||
|
{'>>'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BundleBacktestRequestsTable
|
||||||
|
|
||||||
Reference in New Issue
Block a user