Add admin page for bundle

This commit is contained in:
2025-11-10 11:50:20 +07:00
parent ecf07a7863
commit 0861e9a8d2
18 changed files with 2071 additions and 49 deletions

View 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);
}
}

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -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();
} }

View File

@@ -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();
} }
} }

View File

@@ -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; }
}

View File

@@ -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");
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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; }
}
} }

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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 = () => {

View File

@@ -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

View File

@@ -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