Add admin page for bundle
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -0,0 +1,534 @@
|
||||
import {useState} from 'react'
|
||||
import {useQuery} from '@tanstack/react-query'
|
||||
|
||||
import useApiUrlStore from '../../../app/store/apiStore'
|
||||
import {
|
||||
AdminClient,
|
||||
BundleBacktestRequestSortableColumn,
|
||||
BundleBacktestRequestStatus
|
||||
} from '../../../generated/ManagingApi'
|
||||
|
||||
import BundleBacktestRequestsTable from './bundleBacktestRequestsTable'
|
||||
|
||||
const BundleBacktestRequestsSettings: React.FC = () => {
|
||||
const { apiUrl } = useApiUrlStore()
|
||||
const [page, setPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(50)
|
||||
const [sortBy, setSortBy] = useState<BundleBacktestRequestSortableColumn>(BundleBacktestRequestSortableColumn.CreatedAt)
|
||||
const [sortOrder, setSortOrder] = useState<string>('desc')
|
||||
const [nameContains, setNameContains] = useState<string>('')
|
||||
const [statusFilter, setStatusFilter] = useState<BundleBacktestRequestStatus | null>(BundleBacktestRequestStatus.Failed)
|
||||
const [userIdFilter, setUserIdFilter] = useState<string>('')
|
||||
const [userNameContains, setUserNameContains] = useState<string>('')
|
||||
const [totalBacktestsMin, setTotalBacktestsMin] = useState<string>('')
|
||||
const [totalBacktestsMax, setTotalBacktestsMax] = useState<string>('')
|
||||
const [completedBacktestsMin, setCompletedBacktestsMin] = useState<string>('')
|
||||
const [completedBacktestsMax, setCompletedBacktestsMax] = useState<string>('')
|
||||
const [progressPercentageMin, setProgressPercentageMin] = useState<string>('')
|
||||
const [progressPercentageMax, setProgressPercentageMax] = useState<string>('')
|
||||
const [filtersOpen, setFiltersOpen] = useState<boolean>(false)
|
||||
const [showTable, setShowTable] = useState<boolean>(true)
|
||||
|
||||
const adminClient = new AdminClient({}, apiUrl)
|
||||
|
||||
// Fetch bundle backtest requests summary statistics
|
||||
const {
|
||||
data: bundleSummary,
|
||||
isLoading: isLoadingSummary
|
||||
} = useQuery({
|
||||
queryKey: ['bundleBacktestRequestsSummary'],
|
||||
queryFn: async () => {
|
||||
return await adminClient.admin_GetBundleBacktestRequestsSummary()
|
||||
},
|
||||
staleTime: 10000, // 10 seconds
|
||||
gcTime: 5 * 60 * 1000,
|
||||
refetchInterval: 5000, // Auto-refresh every 5 seconds
|
||||
})
|
||||
|
||||
const {
|
||||
data: bundleRequestsData,
|
||||
isLoading,
|
||||
error,
|
||||
refetch
|
||||
} = useQuery({
|
||||
queryKey: ['bundleBacktestRequests', page, pageSize, sortBy, sortOrder, nameContains, statusFilter, userIdFilter, userNameContains, totalBacktestsMin, totalBacktestsMax, completedBacktestsMin, completedBacktestsMax, progressPercentageMin, progressPercentageMax],
|
||||
queryFn: async () => {
|
||||
return await adminClient.admin_GetBundleBacktestRequestsPaginated(
|
||||
page,
|
||||
pageSize,
|
||||
sortBy,
|
||||
sortOrder,
|
||||
nameContains || null,
|
||||
statusFilter || null,
|
||||
userIdFilter ? parseInt(userIdFilter) : null,
|
||||
userNameContains || null,
|
||||
totalBacktestsMin ? parseInt(totalBacktestsMin) : null,
|
||||
totalBacktestsMax ? parseInt(totalBacktestsMax) : null,
|
||||
completedBacktestsMin ? parseInt(completedBacktestsMin) : null,
|
||||
completedBacktestsMax ? parseInt(completedBacktestsMax) : null,
|
||||
progressPercentageMin ? parseFloat(progressPercentageMin) : null,
|
||||
progressPercentageMax ? parseFloat(progressPercentageMax) : null,
|
||||
null, // createdAtFrom
|
||||
null // createdAtTo
|
||||
)
|
||||
},
|
||||
enabled: showTable,
|
||||
staleTime: 10000, // 10 seconds
|
||||
gcTime: 5 * 60 * 1000,
|
||||
refetchInterval: showTable ? 5000 : false, // Auto-refresh every 5 seconds when table is shown
|
||||
})
|
||||
|
||||
const bundleRequests = bundleRequestsData?.bundleRequests || []
|
||||
const totalCount = bundleRequestsData?.totalCount || 0
|
||||
const totalPages = bundleRequestsData?.totalPages || 0
|
||||
const currentPage = bundleRequestsData?.currentPage || 1
|
||||
|
||||
const handlePageChange = (newPage: number) => {
|
||||
setPage(newPage)
|
||||
}
|
||||
|
||||
const handleSortChange = (newSortBy: BundleBacktestRequestSortableColumn) => {
|
||||
if (sortBy === newSortBy) {
|
||||
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')
|
||||
} else {
|
||||
setSortBy(newSortBy)
|
||||
setSortOrder('desc')
|
||||
}
|
||||
}
|
||||
|
||||
const handleFilterChange = () => {
|
||||
setPage(1) // Reset to first page when filters change
|
||||
}
|
||||
|
||||
const clearFilters = () => {
|
||||
setNameContains('')
|
||||
setStatusFilter(null)
|
||||
setUserIdFilter('')
|
||||
setUserNameContains('')
|
||||
setTotalBacktestsMin('')
|
||||
setTotalBacktestsMax('')
|
||||
setCompletedBacktestsMin('')
|
||||
setCompletedBacktestsMax('')
|
||||
setProgressPercentageMin('')
|
||||
setProgressPercentageMax('')
|
||||
setPage(1)
|
||||
}
|
||||
|
||||
// Helper function to get status badge color
|
||||
const getStatusBadgeColor = (status: string | undefined) => {
|
||||
if (!status) return 'badge-ghost'
|
||||
const statusLower = status.toLowerCase()
|
||||
switch (statusLower) {
|
||||
case 'pending':
|
||||
return 'badge-warning'
|
||||
case 'running':
|
||||
return 'badge-info'
|
||||
case 'completed':
|
||||
return 'badge-success'
|
||||
case 'failed':
|
||||
return 'badge-error'
|
||||
case 'saved':
|
||||
return 'badge-ghost'
|
||||
case 'cancelled':
|
||||
return 'badge-ghost'
|
||||
default:
|
||||
return 'badge-ghost'
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to get status info (icon, description, color)
|
||||
const getStatusInfo = (status: string) => {
|
||||
const statusLower = status.toLowerCase()
|
||||
let statusIcon, statusDesc, statusColor
|
||||
|
||||
switch (statusLower) {
|
||||
case 'pending':
|
||||
statusIcon = (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" className="w-6 h-6">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
)
|
||||
statusDesc = 'Waiting to be processed'
|
||||
statusColor = 'text-warning'
|
||||
break
|
||||
case 'running':
|
||||
statusIcon = (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" className="w-6 h-6">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.348a1.125 1.125 0 010 1.971l-11.54 6.347a1.125 1.125 0 01-1.667-.986V5.653z" />
|
||||
</svg>
|
||||
)
|
||||
statusDesc = 'Currently processing'
|
||||
statusColor = 'text-info'
|
||||
break
|
||||
case 'completed':
|
||||
statusIcon = (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" className="w-6 h-6">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
)
|
||||
statusDesc = 'Successfully finished'
|
||||
statusColor = 'text-success'
|
||||
break
|
||||
case 'failed':
|
||||
statusIcon = (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" className="w-6 h-6">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" />
|
||||
</svg>
|
||||
)
|
||||
statusDesc = 'Requires attention'
|
||||
statusColor = 'text-error'
|
||||
break
|
||||
case 'saved':
|
||||
statusIcon = (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" className="w-6 h-6">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M17.593 3.322c1.1.128 1.907 1.077 1.907 2.185V21L12 17.25 4.5 21V5.507c0-1.108.806-2.057 1.907-2.185a48.507 48.507 0 0111.186 0z" />
|
||||
</svg>
|
||||
)
|
||||
statusDesc = 'Saved as template'
|
||||
statusColor = 'text-neutral'
|
||||
break
|
||||
case 'cancelled':
|
||||
statusIcon = (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" className="w-6 h-6">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
)
|
||||
statusDesc = 'Cancelled by user'
|
||||
statusColor = 'text-neutral'
|
||||
break
|
||||
default:
|
||||
statusIcon = (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" className="w-6 h-6">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9 5.25h.008v.008H12v-.008z" />
|
||||
</svg>
|
||||
)
|
||||
statusDesc = 'Unknown status'
|
||||
statusColor = 'text-base-content'
|
||||
}
|
||||
|
||||
return { statusIcon, statusDesc, statusColor }
|
||||
}
|
||||
|
||||
// All possible statuses for skeleton display
|
||||
const allStatuses = ['Pending', 'Running', 'Completed', 'Failed', 'Saved', 'Cancelled']
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-4 pb-20">
|
||||
{/* Bundle Backtest Requests Summary Statistics */}
|
||||
<div className="mb-8">
|
||||
<div className="space-y-6">
|
||||
{/* Status Overview Section */}
|
||||
<div className="card bg-base-100 shadow-md">
|
||||
<div className="card-body">
|
||||
<h3 className="card-title text-xl mb-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" className="w-6 h-6">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 00-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75 2.25 2.25 0 00-.1-.664m-5.8 0A2.251 2.251 0 0113.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25zM6.75 12h.008v.008H6.75V12zm0 3h.008v.008H6.75V15zm0 3h.008v.008H6.75V18z" />
|
||||
</svg>
|
||||
Status Overview
|
||||
{isLoadingSummary && (
|
||||
<span className="loading loading-spinner loading-sm ml-2"></span>
|
||||
)}
|
||||
</h3>
|
||||
<div className="grid grid-cols-3 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4">
|
||||
{isLoadingSummary ? (
|
||||
// Show skeleton with all statuses set to 0
|
||||
<>
|
||||
{allStatuses.map((status) => {
|
||||
const { statusIcon, statusDesc, statusColor } = getStatusInfo(status)
|
||||
return (
|
||||
<div key={status} className="card bg-base-200 shadow-sm">
|
||||
<div className="card-body p-4">
|
||||
<div className="stat p-0">
|
||||
<div className={`stat-figure ${statusColor} opacity-80 hidden md:block`}>
|
||||
{statusIcon}
|
||||
</div>
|
||||
<div className="stat-title">{status}</div>
|
||||
<div className={`stat-value ${statusColor} text-2xl`}>0</div>
|
||||
<div className="stat-desc text-xs hidden md:block">{statusDesc}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{/* Total card in skeleton */}
|
||||
<div className="card bg-base-200 shadow-sm">
|
||||
<div className="card-body p-4">
|
||||
<div className="stat p-0">
|
||||
<div className="stat-figure text-primary opacity-80 hidden md:block">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" className="w-6 h-6">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 3v11.25A2.25 2.25 0 006 16.5h2.25M3.75 3h-1.5m1.5 0h16.5m0 0h1.5m-1.5 0v11.25A2.25 2.25 0 0118 16.5h-2.25m-7.5 0h7.5m-7.5 0l-1 3m8.5-3l1 3m0 0l.5 1.5m-.5-1.5h-9.5m0 0l-.5 1.5M9 11.25v1.5M12 9v3.75m3-3.75v3.75m-3 .75h.008v.008H12v-.008z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="stat-title">Total</div>
|
||||
<div className="stat-value text-primary text-2xl">0</div>
|
||||
<div className="stat-desc text-xs hidden md:block">Across all statuses</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : bundleSummary?.statusSummary && bundleSummary.statusSummary.length > 0 ? (
|
||||
// Show actual data
|
||||
<>
|
||||
{bundleSummary.statusSummary.map((statusItem) => {
|
||||
const { statusIcon, statusDesc, statusColor } = getStatusInfo(statusItem.status || '')
|
||||
return (
|
||||
<div key={statusItem.status} className="card bg-base-200 shadow-sm">
|
||||
<div className="card-body p-4">
|
||||
<div className="stat p-0">
|
||||
<div className={`stat-figure ${statusColor} opacity-80 hidden md:block`}>
|
||||
{statusIcon}
|
||||
</div>
|
||||
<div className="stat-title">{statusItem.status || 'Unknown'}</div>
|
||||
<div className={`stat-value ${statusColor} text-2xl`}>{statusItem.count || 0}</div>
|
||||
<div className="stat-desc text-xs hidden md:block">{statusDesc}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{/* Total card with actual data */}
|
||||
<div className="card bg-base-200 shadow-sm">
|
||||
<div className="card-body p-4">
|
||||
<div className="stat p-0">
|
||||
<div className="stat-figure text-primary opacity-80 hidden md:block">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" className="w-6 h-6">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 3v11.25A2.25 2.25 0 006 16.5h2.25M3.75 3h-1.5m1.5 0h16.5m0 0h1.5m-1.5 0v11.25A2.25 2.25 0 0118 16.5h-2.25m-7.5 0h7.5m-7.5 0l-1 3m8.5-3l1 3m0 0l.5 1.5m-.5-1.5h-9.5m0 0l-.5 1.5M9 11.25v1.5M12 9v3.75m3-3.75v3.75m-3 .75h.008v.008H12v-.008z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="stat-title">Total</div>
|
||||
<div className="stat-value text-primary text-2xl">{bundleSummary?.totalRequests || 0}</div>
|
||||
<div className="stat-desc text-xs hidden md:block">Across all statuses</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filters Section */}
|
||||
<div className="card bg-base-100 shadow-md mb-4">
|
||||
<div className="card-body py-4">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="card-title text-lg">Filters</h3>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
className="btn btn-sm btn-ghost"
|
||||
onClick={() => setFiltersOpen(!filtersOpen)}
|
||||
>
|
||||
{filtersOpen ? 'Hide' : 'Show'} Filters
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm btn-outline"
|
||||
onClick={clearFilters}
|
||||
>
|
||||
Clear Filters
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{filtersOpen && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text">Name Contains</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by name..."
|
||||
className="input input-bordered input-sm"
|
||||
value={nameContains}
|
||||
onChange={(e) => {
|
||||
setNameContains(e.target.value)
|
||||
handleFilterChange()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text">Status</span>
|
||||
</label>
|
||||
<select
|
||||
className="select select-bordered select-sm"
|
||||
value={statusFilter || ''}
|
||||
onChange={(e) => {
|
||||
setStatusFilter(e.target.value ? (e.target.value as BundleBacktestRequestStatus) : null)
|
||||
handleFilterChange()
|
||||
}}
|
||||
>
|
||||
<option value="">All</option>
|
||||
<option value={BundleBacktestRequestStatus.Pending}>Pending</option>
|
||||
<option value={BundleBacktestRequestStatus.Running}>Running</option>
|
||||
<option value={BundleBacktestRequestStatus.Completed}>Completed</option>
|
||||
<option value={BundleBacktestRequestStatus.Failed}>Failed</option>
|
||||
<option value={BundleBacktestRequestStatus.Saved}>Saved</option>
|
||||
<option value={BundleBacktestRequestStatus.Cancelled}>Cancelled</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text">User ID</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="Filter by user ID..."
|
||||
className="input input-bordered input-sm"
|
||||
value={userIdFilter}
|
||||
onChange={(e) => {
|
||||
setUserIdFilter(e.target.value)
|
||||
handleFilterChange()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text">User Name Contains</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by user name..."
|
||||
className="input input-bordered input-sm"
|
||||
value={userNameContains}
|
||||
onChange={(e) => {
|
||||
setUserNameContains(e.target.value)
|
||||
handleFilterChange()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text">Total Backtests Min</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="Min total backtests..."
|
||||
className="input input-bordered input-sm"
|
||||
value={totalBacktestsMin}
|
||||
onChange={(e) => {
|
||||
setTotalBacktestsMin(e.target.value)
|
||||
handleFilterChange()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text">Total Backtests Max</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="Max total backtests..."
|
||||
className="input input-bordered input-sm"
|
||||
value={totalBacktestsMax}
|
||||
onChange={(e) => {
|
||||
setTotalBacktestsMax(e.target.value)
|
||||
handleFilterChange()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text">Completed Backtests Min</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="Min completed..."
|
||||
className="input input-bordered input-sm"
|
||||
value={completedBacktestsMin}
|
||||
onChange={(e) => {
|
||||
setCompletedBacktestsMin(e.target.value)
|
||||
handleFilterChange()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text">Completed Backtests Max</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="Max completed..."
|
||||
className="input input-bordered input-sm"
|
||||
value={completedBacktestsMax}
|
||||
onChange={(e) => {
|
||||
setCompletedBacktestsMax(e.target.value)
|
||||
handleFilterChange()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text">Progress % Min (0-100)</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="100"
|
||||
step="0.1"
|
||||
placeholder="Min progress %..."
|
||||
className="input input-bordered input-sm"
|
||||
value={progressPercentageMin}
|
||||
onChange={(e) => {
|
||||
setProgressPercentageMin(e.target.value)
|
||||
handleFilterChange()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text">Progress % Max (0-100)</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="100"
|
||||
step="0.1"
|
||||
placeholder="Max progress %..."
|
||||
className="input input-bordered input-sm"
|
||||
value={progressPercentageMax}
|
||||
onChange={(e) => {
|
||||
setProgressPercentageMax(e.target.value)
|
||||
handleFilterChange()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BundleBacktestRequestsTable
|
||||
bundleRequests={bundleRequests}
|
||||
isLoading={isLoading}
|
||||
totalCount={totalCount}
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
pageSize={pageSize}
|
||||
sortBy={sortBy}
|
||||
sortOrder={sortOrder}
|
||||
onPageChange={handlePageChange}
|
||||
onSortChange={handleSortChange}
|
||||
/>
|
||||
|
||||
{error && (
|
||||
<div className="alert alert-error mt-4">
|
||||
<span>Failed to load bundle backtest requests. Please try again.</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BundleBacktestRequestsSettings
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
import React, {useMemo} from 'react'
|
||||
import {
|
||||
type BundleBacktestRequestListItemResponse,
|
||||
BundleBacktestRequestSortableColumn
|
||||
} from '../../../generated/ManagingApi'
|
||||
import {Table} from '../../../components/mollecules'
|
||||
|
||||
interface IBundleBacktestRequestsTable {
|
||||
bundleRequests: BundleBacktestRequestListItemResponse[]
|
||||
isLoading: boolean
|
||||
totalCount: number
|
||||
currentPage: number
|
||||
totalPages: number
|
||||
pageSize: number
|
||||
sortBy: BundleBacktestRequestSortableColumn
|
||||
sortOrder: string
|
||||
onPageChange: (page: number) => void
|
||||
onSortChange: (sortBy: BundleBacktestRequestSortableColumn) => void
|
||||
}
|
||||
|
||||
const BundleBacktestRequestsTable: React.FC<IBundleBacktestRequestsTable> = ({
|
||||
bundleRequests,
|
||||
isLoading,
|
||||
totalCount,
|
||||
currentPage,
|
||||
totalPages,
|
||||
pageSize,
|
||||
sortBy,
|
||||
sortOrder,
|
||||
onPageChange,
|
||||
onSortChange
|
||||
}) => {
|
||||
const getStatusBadge = (status: string | null | undefined) => {
|
||||
if (!status) return <span className="badge badge-sm">-</span>
|
||||
|
||||
const statusLower = status.toLowerCase()
|
||||
switch (statusLower) {
|
||||
case 'pending':
|
||||
return <span className="badge badge-sm badge-warning">Pending</span>
|
||||
case 'running':
|
||||
return <span className="badge badge-sm badge-info">Running</span>
|
||||
case 'completed':
|
||||
return <span className="badge badge-sm badge-success">Completed</span>
|
||||
case 'failed':
|
||||
return <span className="badge badge-sm badge-error">Failed</span>
|
||||
case 'saved':
|
||||
return <span className="badge badge-sm badge-ghost">Saved</span>
|
||||
case 'cancelled':
|
||||
return <span className="badge badge-sm badge-ghost">Cancelled</span>
|
||||
default:
|
||||
return <span className="badge badge-sm">{status}</span>
|
||||
}
|
||||
}
|
||||
|
||||
const formatDate = (date: Date | string | null | undefined) => {
|
||||
if (!date) return '-'
|
||||
try {
|
||||
return new Date(date).toLocaleString()
|
||||
} catch {
|
||||
return '-'
|
||||
}
|
||||
}
|
||||
|
||||
const formatProgress = (progress: number | undefined) => {
|
||||
if (progress === undefined || progress === null) return '-'
|
||||
return `${progress.toFixed(1)}%`
|
||||
}
|
||||
|
||||
const SortableHeader = ({ column, label }: { column: BundleBacktestRequestSortableColumn; label: string }) => {
|
||||
const isActive = sortBy === column
|
||||
return (
|
||||
<div
|
||||
className="flex items-center gap-1 cursor-pointer hover:text-primary text-base-content"
|
||||
onClick={() => onSortChange(column)}
|
||||
>
|
||||
<span className="font-semibold">{label}</span>
|
||||
{isActive && (
|
||||
<span className="text-xs">
|
||||
{sortOrder === 'asc' ? '↑' : '↓'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const columns = useMemo(() => [
|
||||
{
|
||||
id: 'name',
|
||||
Header: () => <SortableHeader column={BundleBacktestRequestSortableColumn.Name} label="Name" />,
|
||||
accessor: (row: BundleBacktestRequestListItemResponse) => (
|
||||
<div>
|
||||
<div className="font-semibold">{row.name || '-'}</div>
|
||||
{row.version && <div className="text-xs text-gray-500">v{row.version}</div>}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'status',
|
||||
Header: () => <SortableHeader column={BundleBacktestRequestSortableColumn.Status} label="Status" />,
|
||||
accessor: (row: BundleBacktestRequestListItemResponse) => getStatusBadge(row.status)
|
||||
},
|
||||
{
|
||||
id: 'user',
|
||||
Header: () => <SortableHeader column={BundleBacktestRequestSortableColumn.UserName} label="User" />,
|
||||
accessor: (row: BundleBacktestRequestListItemResponse) => (
|
||||
<div>
|
||||
{row.userName ? (
|
||||
<>
|
||||
<div className="font-semibold">{row.userName}</div>
|
||||
{row.userId && <div className="text-xs text-gray-500">ID: {row.userId}</div>}
|
||||
</>
|
||||
) : (
|
||||
<span className="text-gray-400">-</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'progress',
|
||||
Header: () => <SortableHeader column={BundleBacktestRequestSortableColumn.ProgressPercentage} label="Progress" />,
|
||||
accessor: (row: BundleBacktestRequestListItemResponse) => (
|
||||
<div>
|
||||
<div className="font-semibold">{formatProgress(row.progressPercentage)}</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
{row.completedBacktests || 0} / {row.totalBacktests || 0}
|
||||
</div>
|
||||
{(row.totalBacktests || 0) > 0 && (
|
||||
<progress
|
||||
className="progress progress-primary w-full h-2 mt-1"
|
||||
value={row.progressPercentage || 0}
|
||||
max={100}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'backtests',
|
||||
Header: () => <SortableHeader column={BundleBacktestRequestSortableColumn.TotalBacktests} label="Backtests" />,
|
||||
accessor: (row: BundleBacktestRequestListItemResponse) => (
|
||||
<div>
|
||||
<div>Total: {row.totalBacktests || 0}</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
Completed: {row.completedBacktests || 0} | Failed: {row.failedBacktests || 0}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'dates',
|
||||
Header: () => <SortableHeader column={BundleBacktestRequestSortableColumn.CreatedAt} label="Dates" />,
|
||||
accessor: (row: BundleBacktestRequestListItemResponse) => (
|
||||
<div className="text-xs">
|
||||
<div>Created: {formatDate(row.createdAt)}</div>
|
||||
{row.completedAt && (
|
||||
<div className="text-gray-500">Completed: {formatDate(row.completedAt)}</div>
|
||||
)}
|
||||
{row.updatedAt && (
|
||||
<div className="text-gray-400">Updated: {formatDate(row.updatedAt)}</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'error',
|
||||
Header: 'Error',
|
||||
accessor: (row: BundleBacktestRequestListItemResponse) => (
|
||||
row.errorMessage ? (
|
||||
<div className="tooltip tooltip-left" data-tip={row.errorMessage}>
|
||||
<span className="badge badge-sm badge-error">Error</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-gray-400">-</span>
|
||||
)
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'requestId',
|
||||
Header: () => <SortableHeader column={BundleBacktestRequestSortableColumn.RequestId} label="Request ID" />,
|
||||
accessor: (row: BundleBacktestRequestListItemResponse) => (
|
||||
<span className="font-mono text-xs">{row.requestId?.substring(0, 8)}...</span>
|
||||
)
|
||||
}
|
||||
], [sortBy, sortOrder, onSortChange])
|
||||
|
||||
const tableData = useMemo(() => {
|
||||
return bundleRequests.map((request) => ({
|
||||
...request,
|
||||
key: request.requestId
|
||||
}))
|
||||
}, [bundleRequests])
|
||||
|
||||
return (
|
||||
<div>
|
||||
{isLoading && (
|
||||
<div className="flex justify-center my-4">
|
||||
<span className="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoading && bundleRequests.length === 0 && (
|
||||
<div className="alert alert-info">
|
||||
<span>No bundle backtest requests found.</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoading && bundleRequests.length > 0 && (
|
||||
<>
|
||||
<div className="overflow-x-auto">
|
||||
<Table
|
||||
columns={columns}
|
||||
data={tableData}
|
||||
showPagination={false}
|
||||
hiddenColumns={[]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Pagination Info and Controls */}
|
||||
<div className="mt-4 flex flex-col items-center gap-2">
|
||||
<p className="text-sm text-gray-500">
|
||||
Total requests: {totalCount} | Page {currentPage} of {totalPages}
|
||||
</p>
|
||||
|
||||
{/* Manual Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<div className="flex justify-center items-center gap-2">
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
onClick={() => onPageChange(1)}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
{'<<'}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
onClick={() => onPageChange(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
{'<'}
|
||||
</button>
|
||||
|
||||
{/* Page numbers */}
|
||||
<div className="flex gap-1">
|
||||
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
|
||||
let pageNum
|
||||
if (totalPages <= 5) {
|
||||
pageNum = i + 1
|
||||
} else if (currentPage <= 3) {
|
||||
pageNum = i + 1
|
||||
} else if (currentPage >= totalPages - 2) {
|
||||
pageNum = totalPages - 4 + i
|
||||
} else {
|
||||
pageNum = currentPage - 2 + i
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
key={pageNum}
|
||||
className={`btn btn-sm ${currentPage === pageNum ? 'btn-primary' : ''}`}
|
||||
onClick={() => onPageChange(pageNum)}
|
||||
>
|
||||
{pageNum}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
onClick={() => onPageChange(currentPage + 1)}
|
||||
disabled={currentPage >= totalPages}
|
||||
>
|
||||
{'>'}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
onClick={() => onPageChange(totalPages)}
|
||||
disabled={currentPage >= totalPages}
|
||||
>
|
||||
{'>>'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BundleBacktestRequestsTable
|
||||
|
||||
Reference in New Issue
Block a user