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

@@ -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 {
private http: { fetch(url: RequestInfo, init?: RequestInit): Promise<Response> };
private baseUrl: string;
@@ -4501,6 +4621,69 @@ export interface ExchangeInitializedStatus {
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 {
id: string;
finalPnl: number;
@@ -4937,15 +5120,7 @@ export interface BundleBacktestRequest {
progressInfo?: string | null;
currentBacktest?: string | null;
estimatedTimeRemainingSeconds?: number | null;
}
export enum BundleBacktestRequestStatus {
Pending = "Pending",
Running = "Running",
Completed = "Completed",
Failed = "Failed",
Cancelled = "Cancelled",
Saved = "Saved",
updatedAt: Date;
}
export interface RunBundleBacktestRequest {

View File

@@ -226,6 +226,69 @@ export interface ExchangeInitializedStatus {
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 {
id: string;
finalPnl: number;
@@ -662,15 +725,7 @@ export interface BundleBacktestRequest {
progressInfo?: string | null;
currentBacktest?: string | null;
estimatedTimeRemainingSeconds?: number | null;
}
export enum BundleBacktestRequestStatus {
Pending = "Pending",
Running = "Running",
Completed = "Completed",
Failed = "Failed",
Cancelled = "Cancelled",
Saved = "Saved",
updatedAt: Date;
}
export interface RunBundleBacktestRequest {

View File

@@ -5,6 +5,7 @@ import {Tabs} from '../../components/mollecules'
import AccountSettings from './account/accountSettings'
import WhitelistSettings from './whitelist/whitelistSettings'
import JobsSettings from './jobs/jobsSettings'
import BundleBacktestRequestsSettings from './bundleBacktestRequests/bundleBacktestRequestsSettings'
type TabsType = {
label: string
@@ -29,6 +30,11 @@ const tabs: TabsType = [
index: 3,
label: 'Jobs',
},
{
Component: BundleBacktestRequestsSettings,
index: 4,
label: 'Bundle',
},
]
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