Add jobs
This commit is contained in:
@@ -78,7 +78,7 @@
|
||||
"@vitejs/plugin-react": "^1.3.2",
|
||||
"all-contributors-cli": "^6.20.0",
|
||||
"autoprefixer": "^10.4.7",
|
||||
"daisyui": "^3.5.1",
|
||||
"daisyui": "^5.4.7",
|
||||
"postcss": "^8.4.13",
|
||||
"prettier": "^2.6.1",
|
||||
"prettier-plugin-tailwind-css": "^1.5.0",
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from 'react'
|
||||
|
||||
interface BottomMenuBarProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const BottomMenuBar: React.FC<BottomMenuBarProps> = ({ children }) => {
|
||||
return (
|
||||
<ul className="menu menu-horizontal bg-base-200 rounded-box fixed bottom-4 left-1/2 transform -translate-x-1/2 shadow-2xl z-50">
|
||||
{children}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
export default BottomMenuBar
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default } from './BottomMenuBar'
|
||||
|
||||
@@ -36,7 +36,7 @@ const Tabs: FC<ITabsProps> = ({
|
||||
{tabs.map((tab: any) => (
|
||||
<button
|
||||
className={
|
||||
'tab whitespace-nowrap flex-shrink-0 px-5 py-2.5 text-sm font-medium transition-all duration-200 rounded-md ' +
|
||||
'tab whitespace-nowrap flex-shrink-0 px-5 py-2.5 text-sm font-medium transition-all duration-200 rounded-md flex items-center justify-center ' +
|
||||
(selectedTab === tab.index
|
||||
? 'tab-active bg-primary text-primary-content shadow-md font-semibold'
|
||||
: 'text-base-content/70 hover:text-base-content hover:bg-base-300/50')
|
||||
@@ -55,7 +55,7 @@ const Tabs: FC<ITabsProps> = ({
|
||||
))}
|
||||
{addButton && (
|
||||
<button
|
||||
className="tab whitespace-nowrap flex-shrink-0 px-5 py-2.5 text-sm font-medium transition-all duration-200 rounded-md text-base-content/70 hover:text-base-content hover:bg-base-300/50"
|
||||
className="tab whitespace-nowrap flex-shrink-0 px-5 py-2.5 text-sm font-medium transition-all duration-200 rounded-md flex items-center justify-center text-base-content/70 hover:text-base-content hover:bg-base-300/50"
|
||||
onClick={onAddButton}
|
||||
key={'add'}
|
||||
type="button"
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import React from 'react'
|
||||
|
||||
export interface UserAction {
|
||||
label: string
|
||||
icon: React.ReactNode
|
||||
onClick: () => void
|
||||
color?: 'primary' | 'secondary' | 'accent' | 'info' | 'success' | 'warning' | 'error' | 'ghost'
|
||||
disabled?: boolean
|
||||
loading?: boolean
|
||||
showCondition?: boolean
|
||||
tooltipPosition?: 'left' | 'right' | 'top' | 'bottom'
|
||||
}
|
||||
|
||||
export interface UserActionsButtonProps {
|
||||
mainAction: UserAction
|
||||
actions: UserAction[]
|
||||
mainButtonIcon: React.ReactNode
|
||||
mainButtonColor?: 'primary' | 'secondary' | 'accent' | 'info' | 'success' | 'warning' | 'error' | 'ghost'
|
||||
position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left'
|
||||
className?: string
|
||||
}
|
||||
|
||||
const UserActionsButton: React.FC<UserActionsButtonProps> = ({
|
||||
mainAction,
|
||||
actions,
|
||||
mainButtonIcon,
|
||||
mainButtonColor = 'info',
|
||||
position = 'bottom-right',
|
||||
className = '',
|
||||
}) => {
|
||||
const getPositionClasses = () => {
|
||||
switch (position) {
|
||||
case 'bottom-right':
|
||||
return 'fixed bottom-6 right-6'
|
||||
case 'bottom-left':
|
||||
return 'fixed bottom-6 left-6'
|
||||
case 'top-right':
|
||||
return 'fixed top-6 right-6'
|
||||
case 'top-left':
|
||||
return 'fixed top-6 left-6'
|
||||
default:
|
||||
return 'fixed bottom-6 right-6'
|
||||
}
|
||||
}
|
||||
|
||||
const getButtonColorClass = (color?: string) => {
|
||||
if (!color) return ''
|
||||
return `btn-${color}`
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`fab fab-flower ${getPositionClasses()} z-50 ${className}`}>
|
||||
{/* Speed Dial buttons - arranged in quarter circle */}
|
||||
{actions.map((action, index) => {
|
||||
// Skip rendering if showCondition is false
|
||||
if (action.showCondition === false) {
|
||||
return null
|
||||
}
|
||||
|
||||
const tooltipPosition = action.tooltipPosition || (position.includes('right') ? 'left' : 'right')
|
||||
|
||||
return (
|
||||
<div key={index} className={`tooltip tooltip-${tooltipPosition}`} data-tip={action.label}>
|
||||
<button
|
||||
className={`btn btn-lg btn-circle ${getButtonColorClass(action.color)}`}
|
||||
onClick={action.onClick}
|
||||
disabled={action.disabled}
|
||||
aria-label={action.label}
|
||||
>
|
||||
{action.loading ? (
|
||||
<span className="loading loading-spinner loading-sm"></span>
|
||||
) : (
|
||||
action.icon
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default UserActionsButton
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export { default } from './UserActionsButton'
|
||||
export type { UserActionsButtonProps, UserAction } from './UserActionsButton'
|
||||
|
||||
@@ -15,3 +15,6 @@ export { default as Card } from './Card/Card'
|
||||
export { default as ConfigDisplayModal } from './ConfigDisplayModal/ConfigDisplayModal'
|
||||
export { default as IndicatorsDisplay } from './IndicatorsDisplay/IndicatorsDisplay'
|
||||
export { default as PlatformLineChart } from './PlatformLineChart/PlatformLineChart'
|
||||
export { default as UserActionsButton } from './UserActionsButton/UserActionsButton'
|
||||
export type { UserActionsButtonProps, UserAction } from './UserActionsButton/UserActionsButton'
|
||||
export { default as BottomMenuBar } from './BottomMenuBar/BottomMenuBar'
|
||||
|
||||
@@ -160,11 +160,12 @@ interface BacktestTableProps {
|
||||
durationMinDays?: number | null
|
||||
durationMaxDays?: number | null
|
||||
}
|
||||
openFiltersTrigger?: number // When this changes, open the filter sidebar
|
||||
}
|
||||
|
||||
|
||||
|
||||
const BacktestTable: React.FC<BacktestTableProps> = ({list, isFetching, onSortChange, currentSort, onBacktestDeleted, onFiltersChange, filters}) => {
|
||||
const BacktestTable: React.FC<BacktestTableProps> = ({list, isFetching, onSortChange, currentSort, onBacktestDeleted, onFiltersChange, filters, openFiltersTrigger}) => {
|
||||
const [rows, setRows] = useState<LightBacktestResponse[]>([])
|
||||
const {apiUrl} = useApiUrlStore()
|
||||
const {removeBacktest} = useBacktestStore()
|
||||
@@ -198,6 +199,41 @@ const BacktestTable: React.FC<BacktestTableProps> = ({list, isFetching, onSortCh
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
|
||||
const [isDeleting, setIsDeleting] = useState(false)
|
||||
|
||||
// Clear all filters function
|
||||
const clearAllFilters = () => {
|
||||
if (!onFiltersChange) return
|
||||
onFiltersChange({
|
||||
nameContains: null,
|
||||
scoreMin: null,
|
||||
scoreMax: null,
|
||||
winrateMin: null,
|
||||
winrateMax: null,
|
||||
maxDrawdownMax: null,
|
||||
tickers: null,
|
||||
indicators: null,
|
||||
durationMinDays: null,
|
||||
durationMaxDays: null,
|
||||
})
|
||||
// Reset local state
|
||||
setNameContains('')
|
||||
setScoreMin(0)
|
||||
setScoreMax(100)
|
||||
setWinMin(0)
|
||||
setWinMax(100)
|
||||
setMaxDrawdownMax('')
|
||||
setTickersInput('')
|
||||
setSelectedIndicators([])
|
||||
setDurationMinDays(null)
|
||||
setDurationMaxDays(null)
|
||||
}
|
||||
|
||||
// Refresh data function
|
||||
const refreshData = () => {
|
||||
if (onBacktestDeleted) {
|
||||
onBacktestDeleted()
|
||||
}
|
||||
}
|
||||
|
||||
const applyFilters = () => {
|
||||
if (!onFiltersChange) return
|
||||
onFiltersChange({
|
||||
@@ -299,6 +335,13 @@ const BacktestTable: React.FC<BacktestTableProps> = ({list, isFetching, onSortCh
|
||||
setDurationMaxDays(filters.durationMaxDays ?? null)
|
||||
}, [filters])
|
||||
|
||||
// Handle external trigger to open filters
|
||||
useEffect(() => {
|
||||
if (openFiltersTrigger && openFiltersTrigger > 0) {
|
||||
setIsFilterOpen(true)
|
||||
}
|
||||
}, [openFiltersTrigger])
|
||||
|
||||
// Handle sort change
|
||||
const handleSortChange = (columnId: string, sortOrder: 'asc' | 'desc') => {
|
||||
if (!onSortChange) return;
|
||||
|
||||
@@ -1072,6 +1072,44 @@ export class BacktestClient extends AuthorizedApiBase {
|
||||
return Promise.resolve<FileResponse>(null as any);
|
||||
}
|
||||
|
||||
backtest_GetBundleStatus(bundleRequestId: string): Promise<BundleBacktestStatusResponse> {
|
||||
let url_ = this.baseUrl + "/Backtest/Bundle/{bundleRequestId}/Status";
|
||||
if (bundleRequestId === undefined || bundleRequestId === null)
|
||||
throw new Error("The parameter 'bundleRequestId' must be defined.");
|
||||
url_ = url_.replace("{bundleRequestId}", encodeURIComponent("" + bundleRequestId));
|
||||
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.processBacktest_GetBundleStatus(_response);
|
||||
});
|
||||
}
|
||||
|
||||
protected processBacktest_GetBundleStatus(response: Response): Promise<BundleBacktestStatusResponse> {
|
||||
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 BundleBacktestStatusResponse;
|
||||
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<BundleBacktestStatusResponse>(null as any);
|
||||
}
|
||||
|
||||
backtest_RunGenetic(request: RunGeneticRequest): Promise<GeneticRequest> {
|
||||
let url_ = this.baseUrl + "/Backtest/Genetic";
|
||||
url_ = url_.replace(/[?&]$/, "");
|
||||
@@ -2354,6 +2392,152 @@ export class DataClient extends AuthorizedApiBase {
|
||||
}
|
||||
}
|
||||
|
||||
export class JobClient 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";
|
||||
}
|
||||
|
||||
job_GetJobStatus(jobId: string): Promise<BacktestJobStatusResponse> {
|
||||
let url_ = this.baseUrl + "/Job/{jobId}";
|
||||
if (jobId === undefined || jobId === null)
|
||||
throw new Error("The parameter 'jobId' must be defined.");
|
||||
url_ = url_.replace("{jobId}", encodeURIComponent("" + jobId));
|
||||
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.processJob_GetJobStatus(_response);
|
||||
});
|
||||
}
|
||||
|
||||
protected processJob_GetJobStatus(response: Response): Promise<BacktestJobStatusResponse> {
|
||||
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 BacktestJobStatusResponse;
|
||||
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<BacktestJobStatusResponse>(null as any);
|
||||
}
|
||||
|
||||
job_GetJobs(page: number | undefined, pageSize: number | undefined, sortBy: string | undefined, sortOrder: string | undefined, status: string | null | undefined, jobType: string | null | undefined, userId: number | null | undefined, workerId: string | null | undefined, bundleRequestId: string | null | undefined): Promise<PaginatedJobsResponse> {
|
||||
let url_ = this.baseUrl + "/Job?";
|
||||
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 === null)
|
||||
throw new Error("The parameter 'sortOrder' cannot be null.");
|
||||
else if (sortOrder !== undefined)
|
||||
url_ += "sortOrder=" + encodeURIComponent("" + sortOrder) + "&";
|
||||
if (status !== undefined && status !== null)
|
||||
url_ += "status=" + encodeURIComponent("" + status) + "&";
|
||||
if (jobType !== undefined && jobType !== null)
|
||||
url_ += "jobType=" + encodeURIComponent("" + jobType) + "&";
|
||||
if (userId !== undefined && userId !== null)
|
||||
url_ += "userId=" + encodeURIComponent("" + userId) + "&";
|
||||
if (workerId !== undefined && workerId !== null)
|
||||
url_ += "workerId=" + encodeURIComponent("" + workerId) + "&";
|
||||
if (bundleRequestId !== undefined && bundleRequestId !== null)
|
||||
url_ += "bundleRequestId=" + encodeURIComponent("" + bundleRequestId) + "&";
|
||||
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.processJob_GetJobs(_response);
|
||||
});
|
||||
}
|
||||
|
||||
protected processJob_GetJobs(response: Response): Promise<PaginatedJobsResponse> {
|
||||
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 PaginatedJobsResponse;
|
||||
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<PaginatedJobsResponse>(null as any);
|
||||
}
|
||||
|
||||
job_GetJobSummary(): Promise<JobSummaryResponse> {
|
||||
let url_ = this.baseUrl + "/Job/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.processJob_GetJobSummary(_response);
|
||||
});
|
||||
}
|
||||
|
||||
protected processJob_GetJobSummary(response: Response): Promise<JobSummaryResponse> {
|
||||
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 JobSummaryResponse;
|
||||
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<JobSummaryResponse>(null as any);
|
||||
}
|
||||
}
|
||||
|
||||
export class MoneyManagementClient extends AuthorizedApiBase {
|
||||
private http: { fetch(url: RequestInfo, init?: RequestInit): Promise<Response> };
|
||||
private baseUrl: string;
|
||||
@@ -4740,6 +4924,20 @@ export interface BundleBacktestRequestViewModel {
|
||||
estimatedTimeRemainingSeconds?: number | null;
|
||||
}
|
||||
|
||||
export interface BundleBacktestStatusResponse {
|
||||
bundleRequestId?: string;
|
||||
status?: string | null;
|
||||
totalJobs?: number;
|
||||
completedJobs?: number;
|
||||
failedJobs?: number;
|
||||
runningJobs?: number;
|
||||
pendingJobs?: number;
|
||||
progressPercentage?: number;
|
||||
createdAt?: Date;
|
||||
completedAt?: Date | null;
|
||||
errorMessage?: string | null;
|
||||
}
|
||||
|
||||
export interface GeneticRequest {
|
||||
requestId: string;
|
||||
user: User;
|
||||
@@ -5199,6 +5397,69 @@ export interface AgentBalance {
|
||||
time?: Date;
|
||||
}
|
||||
|
||||
export interface BacktestJobStatusResponse {
|
||||
jobId?: string;
|
||||
status?: string | null;
|
||||
progressPercentage?: number;
|
||||
createdAt?: Date;
|
||||
startedAt?: Date | null;
|
||||
completedAt?: Date | null;
|
||||
errorMessage?: string | null;
|
||||
result?: LightBacktest | null;
|
||||
}
|
||||
|
||||
export interface PaginatedJobsResponse {
|
||||
jobs?: JobListItemResponse[];
|
||||
totalCount?: number;
|
||||
currentPage?: number;
|
||||
pageSize?: number;
|
||||
totalPages?: number;
|
||||
hasNextPage?: boolean;
|
||||
hasPreviousPage?: boolean;
|
||||
}
|
||||
|
||||
export interface JobListItemResponse {
|
||||
jobId?: string;
|
||||
status?: string;
|
||||
jobType?: string;
|
||||
progressPercentage?: number;
|
||||
priority?: number;
|
||||
userId?: number;
|
||||
bundleRequestId?: string | null;
|
||||
geneticRequestId?: string | null;
|
||||
assignedWorkerId?: string | null;
|
||||
createdAt?: Date;
|
||||
startedAt?: Date | null;
|
||||
completedAt?: Date | null;
|
||||
lastHeartbeat?: Date | null;
|
||||
errorMessage?: string | null;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
}
|
||||
|
||||
export interface JobSummaryResponse {
|
||||
statusSummary?: JobStatusSummary[];
|
||||
jobTypeSummary?: JobTypeSummary[];
|
||||
statusTypeSummary?: JobStatusTypeSummary[];
|
||||
totalJobs?: number;
|
||||
}
|
||||
|
||||
export interface JobStatusSummary {
|
||||
status?: string;
|
||||
count?: number;
|
||||
}
|
||||
|
||||
export interface JobTypeSummary {
|
||||
jobType?: string;
|
||||
count?: number;
|
||||
}
|
||||
|
||||
export interface JobStatusTypeSummary {
|
||||
status?: string;
|
||||
jobType?: string;
|
||||
count?: number;
|
||||
}
|
||||
|
||||
export interface ScenarioViewModel {
|
||||
name: string;
|
||||
indicators: IndicatorViewModel[];
|
||||
|
||||
@@ -735,6 +735,20 @@ export interface BundleBacktestRequestViewModel {
|
||||
estimatedTimeRemainingSeconds?: number | null;
|
||||
}
|
||||
|
||||
export interface BundleBacktestStatusResponse {
|
||||
bundleRequestId?: string;
|
||||
status?: string | null;
|
||||
totalJobs?: number;
|
||||
completedJobs?: number;
|
||||
failedJobs?: number;
|
||||
runningJobs?: number;
|
||||
pendingJobs?: number;
|
||||
progressPercentage?: number;
|
||||
createdAt?: Date;
|
||||
completedAt?: Date | null;
|
||||
errorMessage?: string | null;
|
||||
}
|
||||
|
||||
export interface GeneticRequest {
|
||||
requestId: string;
|
||||
user: User;
|
||||
@@ -1194,6 +1208,69 @@ export interface AgentBalance {
|
||||
time?: Date;
|
||||
}
|
||||
|
||||
export interface BacktestJobStatusResponse {
|
||||
jobId?: string;
|
||||
status?: string | null;
|
||||
progressPercentage?: number;
|
||||
createdAt?: Date;
|
||||
startedAt?: Date | null;
|
||||
completedAt?: Date | null;
|
||||
errorMessage?: string | null;
|
||||
result?: LightBacktest | null;
|
||||
}
|
||||
|
||||
export interface PaginatedJobsResponse {
|
||||
jobs?: JobListItemResponse[];
|
||||
totalCount?: number;
|
||||
currentPage?: number;
|
||||
pageSize?: number;
|
||||
totalPages?: number;
|
||||
hasNextPage?: boolean;
|
||||
hasPreviousPage?: boolean;
|
||||
}
|
||||
|
||||
export interface JobListItemResponse {
|
||||
jobId?: string;
|
||||
status?: string;
|
||||
jobType?: string;
|
||||
progressPercentage?: number;
|
||||
priority?: number;
|
||||
userId?: number;
|
||||
bundleRequestId?: string | null;
|
||||
geneticRequestId?: string | null;
|
||||
assignedWorkerId?: string | null;
|
||||
createdAt?: Date;
|
||||
startedAt?: Date | null;
|
||||
completedAt?: Date | null;
|
||||
lastHeartbeat?: Date | null;
|
||||
errorMessage?: string | null;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
}
|
||||
|
||||
export interface JobSummaryResponse {
|
||||
statusSummary?: JobStatusSummary[];
|
||||
jobTypeSummary?: JobTypeSummary[];
|
||||
statusTypeSummary?: JobStatusTypeSummary[];
|
||||
totalJobs?: number;
|
||||
}
|
||||
|
||||
export interface JobStatusSummary {
|
||||
status?: string;
|
||||
count?: number;
|
||||
}
|
||||
|
||||
export interface JobTypeSummary {
|
||||
jobType?: string;
|
||||
count?: number;
|
||||
}
|
||||
|
||||
export interface JobStatusTypeSummary {
|
||||
status?: string;
|
||||
jobType?: string;
|
||||
count?: number;
|
||||
}
|
||||
|
||||
export interface ScenarioViewModel {
|
||||
name: string;
|
||||
indicators: IndicatorViewModel[];
|
||||
|
||||
@@ -4,6 +4,7 @@ import {Tabs} from '../../components/mollecules'
|
||||
|
||||
import AccountSettings from './account/accountSettings'
|
||||
import WhitelistSettings from './whitelist/whitelistSettings'
|
||||
import JobsSettings from './jobs/jobsSettings'
|
||||
|
||||
type TabsType = {
|
||||
label: string
|
||||
@@ -23,6 +24,11 @@ const tabs: TabsType = [
|
||||
index: 2,
|
||||
label: 'Account',
|
||||
},
|
||||
{
|
||||
Component: JobsSettings,
|
||||
index: 3,
|
||||
label: 'Jobs',
|
||||
},
|
||||
]
|
||||
|
||||
const Admin: React.FC = () => {
|
||||
|
||||
510
src/Managing.WebApp/src/pages/adminPage/jobs/jobsSettings.tsx
Normal file
510
src/Managing.WebApp/src/pages/adminPage/jobs/jobsSettings.tsx
Normal file
@@ -0,0 +1,510 @@
|
||||
import {useState} from 'react'
|
||||
import {useQuery} from '@tanstack/react-query'
|
||||
|
||||
import useApiUrlStore from '../../../app/store/apiStore'
|
||||
import {JobClient} from '../../../generated/ManagingApi'
|
||||
import {BottomMenuBar} from '../../../components/mollecules'
|
||||
|
||||
import JobsTable from './jobsTable'
|
||||
|
||||
const JobsSettings: React.FC = () => {
|
||||
const { apiUrl } = useApiUrlStore()
|
||||
const [page, setPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(50)
|
||||
const [sortBy, setSortBy] = useState<string>('CreatedAt')
|
||||
const [sortOrder, setSortOrder] = useState<string>('desc')
|
||||
const [statusFilter, setStatusFilter] = useState<string>('Pending')
|
||||
const [jobTypeFilter, setJobTypeFilter] = useState<string>('')
|
||||
const [userIdFilter, setUserIdFilter] = useState<string>('')
|
||||
const [workerIdFilter, setWorkerIdFilter] = useState<string>('')
|
||||
const [bundleRequestIdFilter, setBundleRequestIdFilter] = useState<string>('')
|
||||
const [filtersOpen, setFiltersOpen] = useState<boolean>(false)
|
||||
|
||||
const jobClient = new JobClient({}, apiUrl)
|
||||
|
||||
// Fetch job summary statistics
|
||||
const {
|
||||
data: jobSummary,
|
||||
isLoading: isLoadingSummary
|
||||
} = useQuery({
|
||||
queryKey: ['jobSummary'],
|
||||
queryFn: async () => {
|
||||
return await jobClient.job_GetJobSummary()
|
||||
},
|
||||
staleTime: 10000, // 10 seconds
|
||||
gcTime: 5 * 60 * 1000,
|
||||
refetchInterval: 5000, // Auto-refresh every 5 seconds
|
||||
})
|
||||
|
||||
const {
|
||||
data: jobsData,
|
||||
isLoading,
|
||||
error,
|
||||
refetch
|
||||
} = useQuery({
|
||||
queryKey: ['jobs', page, pageSize, sortBy, sortOrder, statusFilter, jobTypeFilter, userIdFilter, workerIdFilter, bundleRequestIdFilter],
|
||||
queryFn: async () => {
|
||||
return await jobClient.job_GetJobs(
|
||||
page,
|
||||
pageSize,
|
||||
sortBy,
|
||||
sortOrder,
|
||||
statusFilter || null,
|
||||
jobTypeFilter || null,
|
||||
userIdFilter ? parseInt(userIdFilter) : null,
|
||||
workerIdFilter || null,
|
||||
bundleRequestIdFilter || null
|
||||
)
|
||||
},
|
||||
staleTime: 10000, // 10 seconds
|
||||
gcTime: 5 * 60 * 1000,
|
||||
refetchInterval: 5000, // Auto-refresh every 5 seconds
|
||||
})
|
||||
|
||||
const jobs = jobsData?.jobs || []
|
||||
const totalCount = jobsData?.totalCount || 0
|
||||
const totalPages = jobsData?.totalPages || 0
|
||||
const currentPage = jobsData?.currentPage || 1
|
||||
|
||||
const handlePageChange = (newPage: number) => {
|
||||
setPage(newPage)
|
||||
}
|
||||
|
||||
const handleSortChange = (newSortBy: string) => {
|
||||
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 = () => {
|
||||
setStatusFilter('Pending') // Reset to Pending instead of All
|
||||
setJobTypeFilter('')
|
||||
setUserIdFilter('')
|
||||
setWorkerIdFilter('')
|
||||
setBundleRequestIdFilter('')
|
||||
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 'cancelled':
|
||||
return 'badge-ghost'
|
||||
default:
|
||||
return 'badge-ghost'
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-4 pb-20">
|
||||
{/* Job Summary Statistics */}
|
||||
<div className="mb-8">
|
||||
{isLoadingSummary ? (
|
||||
<div className="flex flex-col items-center justify-center py-12">
|
||||
<progress className="progress progress-primary w-56"></progress>
|
||||
<p className="mt-4 text-base-content/70">Loading job summary...</p>
|
||||
</div>
|
||||
) : jobSummary && (
|
||||
<div className="space-y-6">
|
||||
{/* Status Overview Section */}
|
||||
{jobSummary.statusSummary && jobSummary.statusSummary.length > 0 && (
|
||||
<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
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{jobSummary.statusSummary.map((statusItem) => {
|
||||
const statusLower = (statusItem.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 '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 (
|
||||
<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}`}>
|
||||
{statusIcon}
|
||||
</div>
|
||||
<div className="stat-title">{statusItem.status || 'Unknown'}</div>
|
||||
<div className={`stat-value ${statusColor}`}>{statusItem.count || 0}</div>
|
||||
<div className="stat-desc">{statusDesc}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Job Types Section */}
|
||||
{jobSummary.jobTypeSummary && jobSummary.jobTypeSummary.length > 0 && (
|
||||
<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="M3.75 6A2.25 2.25 0 016 3.75h2.25A2.25 2.25 0 0110.5 6v2.25a2.25 2.25 0 01-2.25 2.25H6a2.25 2.25 0 01-2.25-2.25V6zM3.75 15.75A2.25 2.25 0 016 13.5h2.25a2.25 2.25 0 012.25 2.25V18a2.25 2.25 0 01-2.25 2.25H6A2.25 2.25 0 013.75 18v-2.25zM13.5 6a2.25 2.25 0 012.25-2.25H18A2.25 2.25 0 0120.25 6v2.25A2.25 2.25 0 0118 10.5h-2.25a2.25 2.25 0 01-2.25-2.25V6zM13.5 15.75a2.25 2.25 0 012.25-2.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-2.25A2.25 2.25 0 0113.5 18v-2.25z" />
|
||||
</svg>
|
||||
Job Types
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{jobSummary.jobTypeSummary.map((typeItem) => {
|
||||
const jobTypeLower = (typeItem.jobType || '').toLowerCase()
|
||||
let jobTypeIcon, jobTypeDesc
|
||||
|
||||
switch (jobTypeLower) {
|
||||
case 'backtest':
|
||||
jobTypeIcon = (
|
||||
<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.75 3.104v5.714a2.25 2.25 0 01-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 014.5 0m0 0v5.714c0 .597.237 1.17.659 1.591L19.8 15.3M14.25 3.104c.251.023.501.05.75.082M19.8 15.3l-1.57.393A9.065 9.065 0 0112 15a9.065 9.065 0 00-6.23-.693L5 14.5m14.8.8l1.402 1.402c1.232 1.232 1.232 3.228 0 4.46s-3.228 1.232-4.46 0L14.3 19.8M5 14.5l-1.402 1.402c-1.232 1.232-1.232 3.228 0 4.46s3.228 1.232 4.46 0L9.7 19.8" />
|
||||
</svg>
|
||||
)
|
||||
jobTypeDesc = 'Backtest jobs'
|
||||
break
|
||||
case 'geneticbacktest':
|
||||
jobTypeIcon = (
|
||||
<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.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09zM18.259 8.715L18 9.75l-.259-1.035a3.375 3.375 0 00-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 002.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 002.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 00-2.456 2.456zM16.894 20.567L16.5 21.75l-.394-1.183a2.25 2.25 0 00-1.423-1.423L13.5 18.75l1.183-.394a2.25 2.25 0 001.423-1.423l.394-1.183.394 1.183a2.25 2.25 0 001.423 1.423l1.183.394-1.183.394a2.25 2.25 0 00-1.423 1.423z" />
|
||||
</svg>
|
||||
)
|
||||
jobTypeDesc = 'Genetic backtest jobs'
|
||||
break
|
||||
default:
|
||||
jobTypeIcon = (
|
||||
<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="M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375m16.5 0v3.75m-16.5-3.75v3.75m16.5 0v3.75C20.25 16.153 16.556 18 12 18s-8.25-1.847-8.25-4.125v-3.75m16.5 0c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125" />
|
||||
</svg>
|
||||
)
|
||||
jobTypeDesc = 'Job type'
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={typeItem.jobType} className="card bg-base-200 shadow-sm">
|
||||
<div className="card-body p-4">
|
||||
<div className="stat p-0">
|
||||
<div className="stat-figure text-primary">
|
||||
{jobTypeIcon}
|
||||
</div>
|
||||
<div className="stat-title">{typeItem.jobType || 'Unknown'}</div>
|
||||
<div className="stat-value text-primary">{typeItem.count || 0}</div>
|
||||
<div className="stat-desc">{jobTypeDesc}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Status by Job Type Table Section */}
|
||||
{jobSummary.statusTypeSummary && jobSummary.statusTypeSummary.length > 0 && (
|
||||
<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="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-9 .75h12.75a2.25 2.25 0 002.25-2.25V6.75a2.25 2.25 0 00-2.25-2.25H6.75A2.25 2.25 0 004.5 6.75v7.5a2.25 2.25 0 002.25 2.25z" />
|
||||
</svg>
|
||||
Status by Job Type
|
||||
</h3>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="table table-zebra w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
<th>Job Type</th>
|
||||
<th>Count</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{jobSummary.statusTypeSummary.map((item, index) => (
|
||||
<tr key={`${item.status}-${item.jobType}-${index}`} className="hover">
|
||||
<td>
|
||||
<span className={`badge ${getStatusBadgeColor(item.status)}`}>
|
||||
{item.status || 'Unknown'}
|
||||
</span>
|
||||
</td>
|
||||
<td>{item.jobType || 'Unknown'}</td>
|
||||
<td>{item.count || 0}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{filtersOpen && (
|
||||
<div className="card bg-base-200 mb-4">
|
||||
<div className="card-body">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-4">
|
||||
<div>
|
||||
<label className="label">
|
||||
<span className="label-text">Status</span>
|
||||
</label>
|
||||
<select
|
||||
data-filter="status"
|
||||
className="select select-bordered w-full"
|
||||
value={statusFilter}
|
||||
onChange={(e) => {
|
||||
setStatusFilter(e.target.value)
|
||||
handleFilterChange()
|
||||
}}
|
||||
>
|
||||
<option value="">All</option>
|
||||
<option value="Pending">Pending</option>
|
||||
<option value="Running">Running</option>
|
||||
<option value="Completed">Completed</option>
|
||||
<option value="Failed">Failed</option>
|
||||
<option value="Cancelled">Cancelled</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="label">
|
||||
<span className="label-text">Job Type</span>
|
||||
</label>
|
||||
<select
|
||||
data-filter="jobType"
|
||||
className="select select-bordered w-full"
|
||||
value={jobTypeFilter}
|
||||
onChange={(e) => {
|
||||
setJobTypeFilter(e.target.value)
|
||||
handleFilterChange()
|
||||
}}
|
||||
>
|
||||
<option value="">All</option>
|
||||
<option value="Backtest">Backtest</option>
|
||||
<option value="GeneticBacktest">Genetic Backtest</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="label">
|
||||
<span className="label-text">User ID</span>
|
||||
</label>
|
||||
<input
|
||||
data-filter="userId"
|
||||
type="number"
|
||||
className="input input-bordered w-full"
|
||||
placeholder="User ID"
|
||||
value={userIdFilter}
|
||||
onChange={(e) => {
|
||||
setUserIdFilter(e.target.value)
|
||||
handleFilterChange()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="label">
|
||||
<span className="label-text">Worker ID</span>
|
||||
</label>
|
||||
<input
|
||||
data-filter="workerId"
|
||||
type="text"
|
||||
className="input input-bordered w-full"
|
||||
placeholder="Worker ID"
|
||||
value={workerIdFilter}
|
||||
onChange={(e) => {
|
||||
setWorkerIdFilter(e.target.value)
|
||||
handleFilterChange()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="label">
|
||||
<span className="label-text">Bundle Request ID</span>
|
||||
</label>
|
||||
<input
|
||||
data-filter="bundleRequestId"
|
||||
type="text"
|
||||
className="input input-bordered w-full"
|
||||
placeholder="Bundle Request ID"
|
||||
value={bundleRequestIdFilter}
|
||||
onChange={(e) => {
|
||||
setBundleRequestIdFilter(e.target.value)
|
||||
handleFilterChange()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="alert alert-error mb-4">
|
||||
<span>Error loading jobs: {(error as any)?.message || 'Unknown error'}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<JobsTable
|
||||
jobs={jobs}
|
||||
isLoading={isLoading}
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
totalCount={totalCount}
|
||||
pageSize={pageSize}
|
||||
sortBy={sortBy}
|
||||
sortOrder={sortOrder}
|
||||
onPageChange={handlePageChange}
|
||||
onSortChange={handleSortChange}
|
||||
/>
|
||||
|
||||
{/* Bottom Menu Bar */}
|
||||
<BottomMenuBar>
|
||||
<li>
|
||||
<a
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
setFiltersOpen(!filtersOpen)
|
||||
}}
|
||||
className={filtersOpen ? 'active' : ''}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
refetch()
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
clearFilters()
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
</BottomMenuBar>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default JobsSettings
|
||||
|
||||
313
src/Managing.WebApp/src/pages/adminPage/jobs/jobsTable.tsx
Normal file
313
src/Managing.WebApp/src/pages/adminPage/jobs/jobsTable.tsx
Normal file
@@ -0,0 +1,313 @@
|
||||
import React, {useMemo} from 'react'
|
||||
import {type JobListItemResponse} from '../../../generated/ManagingApi'
|
||||
import {Table} from '../../../components/mollecules'
|
||||
|
||||
interface IJobsTable {
|
||||
jobs: JobListItemResponse[]
|
||||
isLoading: boolean
|
||||
totalCount: number
|
||||
currentPage: number
|
||||
totalPages: number
|
||||
pageSize: number
|
||||
sortBy: string
|
||||
sortOrder: string
|
||||
onPageChange: (page: number) => void
|
||||
onSortChange: (sortBy: string) => void
|
||||
}
|
||||
|
||||
const JobsTable: React.FC<IJobsTable> = ({
|
||||
jobs,
|
||||
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 'cancelled':
|
||||
return <span className="badge badge-sm badge-ghost">Cancelled</span>
|
||||
default:
|
||||
return <span className="badge badge-sm">{status}</span>
|
||||
}
|
||||
}
|
||||
|
||||
const getJobTypeBadge = (jobType: string | null | undefined) => {
|
||||
if (!jobType) return <span className="badge badge-sm">-</span>
|
||||
|
||||
const typeLower = jobType.toLowerCase()
|
||||
switch (typeLower) {
|
||||
case 'backtest':
|
||||
return <span className="badge badge-sm badge-primary">Backtest</span>
|
||||
case 'geneticbacktest':
|
||||
return <span className="badge badge-sm badge-secondary">Genetic</span>
|
||||
default:
|
||||
return <span className="badge badge-sm">{jobType}</span>
|
||||
}
|
||||
}
|
||||
|
||||
const formatDate = (date: Date | string | null | undefined) => {
|
||||
if (!date) return '-'
|
||||
try {
|
||||
return new Date(date).toLocaleString()
|
||||
} catch {
|
||||
return '-'
|
||||
}
|
||||
}
|
||||
|
||||
const SortableHeader = ({ column, label }: { column: string; label: string }) => {
|
||||
const isActive = sortBy === column
|
||||
return (
|
||||
<div
|
||||
className="flex items-center gap-1 cursor-pointer hover:text-primary"
|
||||
onClick={() => onSortChange(column)}
|
||||
>
|
||||
<span>{label}</span>
|
||||
{isActive && (
|
||||
<span className="text-xs">
|
||||
{sortOrder === 'asc' ? '↑' : '↓'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const columns = useMemo(() => [
|
||||
{
|
||||
Header: () => <SortableHeader column="JobId" label="Job ID" />,
|
||||
accessor: 'jobId',
|
||||
width: 200,
|
||||
Cell: ({ value }: any) => (
|
||||
<span className="font-mono text-xs">{value || '-'}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
Header: () => <SortableHeader column="Status" label="Status" />,
|
||||
accessor: 'status',
|
||||
width: 120,
|
||||
Cell: ({ value }: any) => getStatusBadge(value),
|
||||
},
|
||||
{
|
||||
Header: () => <SortableHeader column="JobType" label="Job Type" />,
|
||||
accessor: 'jobType',
|
||||
width: 120,
|
||||
Cell: ({ value }: any) => getJobTypeBadge(value),
|
||||
},
|
||||
{
|
||||
Header: () => <SortableHeader column="Priority" label="Priority" />,
|
||||
accessor: 'priority',
|
||||
width: 100,
|
||||
Cell: ({ value }: any) => (
|
||||
<span className="font-bold">{value ?? 0}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
Header: () => <SortableHeader column="ProgressPercentage" label="Progress" />,
|
||||
accessor: 'progressPercentage',
|
||||
width: 150,
|
||||
Cell: ({ value }: any) => {
|
||||
const percentage = value ?? 0
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<progress
|
||||
className="progress progress-primary w-20"
|
||||
value={percentage}
|
||||
max="100"
|
||||
/>
|
||||
<span className="text-xs">{percentage}%</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
Header: 'User ID',
|
||||
accessor: 'userId',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
Header: 'Worker ID',
|
||||
accessor: 'assignedWorkerId',
|
||||
width: 150,
|
||||
Cell: ({ value }: any) => (
|
||||
<span className="font-mono text-xs">{value || '-'}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
Header: 'Bundle Request ID',
|
||||
accessor: 'bundleRequestId',
|
||||
width: 200,
|
||||
Cell: ({ value }: any) => (
|
||||
<span className="font-mono text-xs">{value || '-'}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
Header: 'Genetic Request ID',
|
||||
accessor: 'geneticRequestId',
|
||||
width: 200,
|
||||
Cell: ({ value }: any) => (
|
||||
<span className="font-mono text-xs">{value || '-'}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
Header: () => <SortableHeader column="CreatedAt" label="Created" />,
|
||||
accessor: 'createdAt',
|
||||
width: 180,
|
||||
Cell: ({ value }: any) => formatDate(value),
|
||||
},
|
||||
{
|
||||
Header: () => <SortableHeader column="StartedAt" label="Started" />,
|
||||
accessor: 'startedAt',
|
||||
width: 180,
|
||||
Cell: ({ value }: any) => formatDate(value),
|
||||
},
|
||||
{
|
||||
Header: () => <SortableHeader column="CompletedAt" label="Completed" />,
|
||||
accessor: 'completedAt',
|
||||
width: 180,
|
||||
Cell: ({ value }: any) => formatDate(value),
|
||||
},
|
||||
{
|
||||
Header: 'Error Message',
|
||||
accessor: 'errorMessage',
|
||||
width: 300,
|
||||
Cell: ({ value }: any) => (
|
||||
<span className="text-xs text-error">{value || '-'}</span>
|
||||
),
|
||||
},
|
||||
], [sortBy, sortOrder, onSortChange])
|
||||
|
||||
const tableData = useMemo(() => {
|
||||
return jobs.map((job) => ({
|
||||
jobId: job.jobId,
|
||||
status: job.status,
|
||||
jobType: job.jobType,
|
||||
priority: job.priority,
|
||||
progressPercentage: job.progressPercentage,
|
||||
userId: job.userId,
|
||||
assignedWorkerId: job.assignedWorkerId,
|
||||
bundleRequestId: job.bundleRequestId,
|
||||
geneticRequestId: job.geneticRequestId,
|
||||
createdAt: job.createdAt,
|
||||
startedAt: job.startedAt,
|
||||
completedAt: job.completedAt,
|
||||
errorMessage: job.errorMessage,
|
||||
startDate: job.startDate,
|
||||
endDate: job.endDate,
|
||||
lastHeartbeat: job.lastHeartbeat,
|
||||
}))
|
||||
}, [jobs])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-4">
|
||||
<p className="text-sm text-gray-500">
|
||||
Total jobs: {totalCount} | Showing {((currentPage - 1) * pageSize) + 1} - {Math.min(currentPage * pageSize, totalCount)} | Page {currentPage} of {totalPages}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{isLoading && (
|
||||
<div className="flex justify-center my-4">
|
||||
<span className="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoading && jobs.length === 0 && (
|
||||
<div className="alert alert-info">
|
||||
<span>No jobs found.</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoading && jobs.length > 0 && (
|
||||
<>
|
||||
<div className="overflow-x-auto">
|
||||
<Table
|
||||
columns={columns}
|
||||
data={tableData}
|
||||
showPagination={false}
|
||||
hiddenColumns={[]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Manual Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<div className="flex justify-center items-center gap-2 mt-4">
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
export default JobsTable
|
||||
|
||||
@@ -7,6 +7,73 @@ import {useEffect, useState} from 'react'
|
||||
import {useAuthStore} from '../../app/store/accountStore'
|
||||
import {ALLOWED_TICKERS, useClaimUiFees, useClaimUiFeesTransaction} from '../../hooks/useClaimUiFees'
|
||||
import Toast from '../../components/mollecules/Toast/Toast'
|
||||
import useApiUrlStore from '../../app/store/apiStore'
|
||||
|
||||
// Environment Badge Component
|
||||
const EnvironmentBadge = ({ environment }: { environment: 'local' | 'sandbox' | 'production' }) => {
|
||||
const badgeColors = {
|
||||
local: 'badge-warning',
|
||||
sandbox: 'badge-info',
|
||||
production: 'badge-success',
|
||||
}
|
||||
|
||||
const badgeLabels = {
|
||||
local: 'Local',
|
||||
sandbox: 'Sandbox',
|
||||
production: 'Production',
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`badge ${badgeColors[environment]} badge-sm font-semibold`}>
|
||||
{badgeLabels[environment]}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Environment Dropdown Component
|
||||
const EnvironmentDropdown = () => {
|
||||
const { environment, setEnvironment } = useApiUrlStore()
|
||||
|
||||
const handleEnvironmentChange = (newEnv: 'local' | 'sandbox' | 'production') => {
|
||||
setEnvironment(newEnv)
|
||||
// Reload page to reinitialize Privy with new app ID
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="dropdown dropdown-end">
|
||||
<label tabIndex={0} className="cursor-pointer">
|
||||
<EnvironmentBadge environment={environment} />
|
||||
</label>
|
||||
<ul tabIndex={0} className="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-40">
|
||||
<li>
|
||||
<a
|
||||
onClick={() => handleEnvironmentChange('local')}
|
||||
className={environment === 'local' ? 'active' : ''}
|
||||
>
|
||||
Local
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
onClick={() => handleEnvironmentChange('sandbox')}
|
||||
className={environment === 'sandbox' ? 'active' : ''}
|
||||
>
|
||||
Sandbox
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
onClick={() => handleEnvironmentChange('production')}
|
||||
className={environment === 'production' ? 'active' : ''}
|
||||
>
|
||||
Production
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const Auth = ({ children }: any) => {
|
||||
const { getCookie, deleteCookie } = useCookie()
|
||||
@@ -90,6 +157,9 @@ export const Auth = ({ children }: any) => {
|
||||
deleteCookie('token')
|
||||
return (
|
||||
<div style={{ ...styles, flexDirection: 'column', gap: '20px' }}>
|
||||
<div style={{ position: 'absolute', top: '20px', right: '20px' }}>
|
||||
<EnvironmentDropdown />
|
||||
</div>
|
||||
<button
|
||||
onClick={login}
|
||||
style={{
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, {useEffect, useRef, useState} from 'react';
|
||||
import {
|
||||
BacktestClient,
|
||||
BundleBacktestRequest,
|
||||
BundleBacktestStatusResponse,
|
||||
BundleBacktestUniversalConfig,
|
||||
DataClient,
|
||||
DateTimeRange,
|
||||
@@ -23,6 +24,7 @@ import BacktestTable from '../../components/organism/Backtest/backtestTable';
|
||||
import FormInput from '../../components/mollecules/FormInput/FormInput';
|
||||
import CustomScenario from '../../components/organism/CustomScenario/CustomScenario';
|
||||
import {useCustomScenario} from '../../app/store/customScenario';
|
||||
import {BottomMenuBar} from '../../components/mollecules';
|
||||
|
||||
interface BundleRequestModalProps {
|
||||
open: boolean;
|
||||
@@ -273,6 +275,21 @@ const BundleRequestModal: React.FC<BundleRequestModalProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
// Bundle status query
|
||||
const {
|
||||
data: bundleStatus,
|
||||
isLoading: isLoadingStatus,
|
||||
error: statusError
|
||||
} = useQuery<BundleBacktestStatusResponse>({
|
||||
queryKey: ['bundle-status', bundle?.requestId],
|
||||
queryFn: async () => {
|
||||
if (!open || !bundle?.requestId) return null;
|
||||
return await backtestClient.backtest_GetBundleStatus(bundle.requestId);
|
||||
},
|
||||
enabled: !!open && !!bundle?.requestId,
|
||||
refetchInterval: 5000, // Poll every 5 seconds for status updates
|
||||
});
|
||||
|
||||
// Existing bundle viewing logic
|
||||
const {
|
||||
data: queryBacktests,
|
||||
@@ -401,6 +418,54 @@ const BundleRequestModal: React.FC<BundleRequestModalProps> = ({
|
||||
<div><b>Created:</b> {bundle.createdAt ? new Date(bundle.createdAt).toLocaleString() : '-'}</div>
|
||||
<div><b>Completed:</b> {bundle.completedAt ? new Date(bundle.completedAt).toLocaleString() : '-'}</div>
|
||||
</div>
|
||||
|
||||
{/* Bundle Status Display */}
|
||||
{bundleStatus && (
|
||||
<div className="mb-4 p-4 bg-base-200 rounded-lg">
|
||||
<h4 className="font-semibold mb-2">Job Progress</h4>
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-2 text-sm">
|
||||
<div>
|
||||
<div className="text-gray-500">Total Jobs</div>
|
||||
<div className="font-bold">{bundleStatus.totalJobs || 0}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-gray-500">Pending</div>
|
||||
<div className="font-bold text-warning">{bundleStatus.pendingJobs || 0}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-gray-500">Running</div>
|
||||
<div className="font-bold text-info">{bundleStatus.runningJobs || 0}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-gray-500">Completed</div>
|
||||
<div className="font-bold text-success">{bundleStatus.completedJobs || 0}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-gray-500">Failed</div>
|
||||
<div className="font-bold text-error">{bundleStatus.failedJobs || 0}</div>
|
||||
</div>
|
||||
</div>
|
||||
{bundleStatus.progressPercentage !== undefined && (
|
||||
<div className="mt-3">
|
||||
<div className="flex justify-between text-xs mb-1">
|
||||
<span>Progress</span>
|
||||
<span>{bundleStatus.progressPercentage}%</span>
|
||||
</div>
|
||||
<progress
|
||||
className="progress progress-primary w-full"
|
||||
value={bundleStatus.progressPercentage}
|
||||
max="100"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{bundleStatus.errorMessage && (
|
||||
<div className="mt-2 text-error text-xs">
|
||||
Error: {bundleStatus.errorMessage}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="divider">Backtest Results</div>
|
||||
{isLoading ? (
|
||||
<div>Loading backtests...</div>
|
||||
@@ -413,6 +478,56 @@ const BundleRequestModal: React.FC<BundleRequestModalProps> = ({
|
||||
<button className="btn" onClick={onClose}>Close</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom Menu Bar */}
|
||||
<BottomMenuBar>
|
||||
<li>
|
||||
<div className="tooltip tooltip-top" data-tip="Refresh">
|
||||
<a
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
refetch()
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div className="tooltip tooltip-top" data-tip="Close">
|
||||
<a
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
</BottomMenuBar>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -845,6 +960,86 @@ const BundleRequestModal: React.FC<BundleRequestModalProps> = ({
|
||||
<button className="btn" onClick={onClose}>Close</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom Menu Bar */}
|
||||
<BottomMenuBar>
|
||||
<li>
|
||||
<div className="tooltip tooltip-top" data-tip="Run Backtest">
|
||||
<a
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
handleCreateBundle(false)
|
||||
}}
|
||||
className={!strategyName || selectedTickers.length === 0 || !scenario ? 'disabled' : ''}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div className="tooltip tooltip-top" data-tip="Save as Template">
|
||||
<a
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
handleCreateBundle(true)
|
||||
}}
|
||||
className={!strategyName || selectedTickers.length === 0 || !scenario ? 'disabled' : ''}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div className="tooltip tooltip-top" data-tip="Close">
|
||||
<a
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
</BottomMenuBar>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import {ColorSwatchIcon, TrashIcon} from '@heroicons/react/solid'
|
||||
import {useQuery, useQueryClient} from '@tanstack/react-query'
|
||||
import React, {useEffect, useState} from 'react'
|
||||
|
||||
import 'react-toastify/dist/ReactToastify.css'
|
||||
import useApiUrlStore from '../../app/store/apiStore'
|
||||
import {Loader, Slider} from '../../components/atoms'
|
||||
import {Modal, Toast} from '../../components/mollecules'
|
||||
import {BottomMenuBar, Modal, Toast} from '../../components/mollecules'
|
||||
import {BacktestTable, UnifiedTradingModal} from '../../components/organism'
|
||||
import type {LightBacktestResponse} from '../../generated/ManagingApi'
|
||||
import {BacktestClient, BacktestSortableColumn} from '../../generated/ManagingApi'
|
||||
@@ -25,6 +24,7 @@ const BacktestScanner: React.FC = () => {
|
||||
sortBy: BacktestSortableColumn.Score,
|
||||
sortOrder: 'desc'
|
||||
})
|
||||
const [openFiltersTrigger, setOpenFiltersTrigger] = useState(0)
|
||||
|
||||
// Filters state coming from BacktestTable sidebar
|
||||
const [filters, setFilters] = useState<{
|
||||
@@ -233,20 +233,7 @@ const BacktestScanner: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto">
|
||||
<div className="tooltip" data-tip="Run backtest">
|
||||
<button className="btn btn-secondary m-1 text-xs" onClick={openModal}>
|
||||
<ColorSwatchIcon width="20"></ColorSwatchIcon>
|
||||
</button>
|
||||
</div>
|
||||
<div className="tooltip" data-tip="Delete all Backtests inferior to 50%">
|
||||
<button
|
||||
className="btn btn-primary m-1 text-xs"
|
||||
onClick={openModalRemoveBacktests}
|
||||
>
|
||||
<TrashIcon width="20"></TrashIcon>
|
||||
</button>
|
||||
</div>
|
||||
<div className="container mx-auto pb-20">
|
||||
|
||||
{/* Selected filters summary */}
|
||||
<div className="mt-2 mb-2">
|
||||
@@ -302,6 +289,7 @@ const BacktestScanner: React.FC = () => {
|
||||
// Invalidate backtest queries when a backtest is deleted
|
||||
queryClient.invalidateQueries({ queryKey: ['backtests'] })
|
||||
}}
|
||||
openFiltersTrigger={openFiltersTrigger}
|
||||
/>
|
||||
{/* Pagination controls */}
|
||||
{totalPages > 1 && (
|
||||
@@ -406,6 +394,107 @@ const BacktestScanner: React.FC = () => {
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* Bottom Menu Bar */}
|
||||
<BottomMenuBar>
|
||||
<li>
|
||||
<div className="tooltip tooltip-top" data-tip="Show Filters">
|
||||
<a
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
setOpenFiltersTrigger(prev => prev + 1)
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div className="tooltip tooltip-top" data-tip="Run Backtest">
|
||||
<a
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
openModal()
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div className="tooltip tooltip-top" data-tip="Delete Backtests by Filters">
|
||||
<a
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
openModalRemoveBacktests()
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div className="tooltip tooltip-top" data-tip="Refresh">
|
||||
<a
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
refetch()
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
</BottomMenuBar>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import Table from '../../components/mollecules/Table/Table';
|
||||
import {BundleBacktestRequest} from '../../generated/ManagingApiTypes';
|
||||
import Toast from '../../components/mollecules/Toast/Toast';
|
||||
import BundleRequestModal from './BundleRequestModal';
|
||||
import {BottomMenuBar} from '../../components/mollecules';
|
||||
|
||||
const BundleRequestsTable = () => {
|
||||
const { apiUrl } = useApiUrlStore();
|
||||
@@ -154,19 +155,7 @@ const BundleRequestsTable = () => {
|
||||
if (error) return <div className="text-error">{error}</div>;
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-lg font-bold">Bundle Backtest Requests</h2>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => {
|
||||
setSelectedBundle(null);
|
||||
setModalOpen(true);
|
||||
}}
|
||||
>
|
||||
Create New Bundle
|
||||
</button>
|
||||
</div>
|
||||
<div className="w-full pb-20">
|
||||
<Table columns={columns} data={data} showPagination={true} />
|
||||
<BundleRequestModal
|
||||
open={modalOpen}
|
||||
@@ -178,6 +167,57 @@ const BundleRequestsTable = () => {
|
||||
fetchData(); // Refresh the table
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Bottom Menu Bar */}
|
||||
<BottomMenuBar>
|
||||
<li>
|
||||
<div className="tooltip tooltip-top" data-tip="Create New Bundle">
|
||||
<a
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
setSelectedBundle(null)
|
||||
setModalOpen(true)
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div className="tooltip tooltip-top" data-tip="Refresh">
|
||||
<a
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
fetchData()
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
</BottomMenuBar>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -115,66 +115,100 @@ function UserInfoSettings() {
|
||||
return (
|
||||
<div className="container mx-auto p-4">
|
||||
<div className="bg-base-200 rounded-lg p-6 shadow-lg">
|
||||
<h2 className="text-2xl font-bold mb-4">User Information</h2>
|
||||
<h2 className="text-2xl font-bold mb-6 text-base-content">User Information</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="font-semibold">Name:</label>
|
||||
<p>{user?.name}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="font-semibold">Agent Name:</label>
|
||||
<p>{user?.agentName || 'Not set'}</p>
|
||||
<button
|
||||
className="btn btn-primary mt-2"
|
||||
onClick={() => setShowUpdateModal(true)}
|
||||
>
|
||||
Update Agent Name
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="font-semibold">Avatar:</label>
|
||||
<div className="mt-2 flex items-center space-x-4">
|
||||
{user?.avatarUrl ? (
|
||||
<img
|
||||
src={user.avatarUrl}
|
||||
alt="User avatar"
|
||||
className="w-16 h-16 rounded-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-16 h-16 rounded-full bg-base-300 flex items-center justify-center">
|
||||
<span className="text-2xl">{user?.name?.[0]?.toUpperCase() || '?'}</span>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => setShowAvatarModal(true)}
|
||||
>
|
||||
Update Avatar
|
||||
</button>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Name Card */}
|
||||
<div className="card bg-base-100 shadow-md">
|
||||
<div className="card-body">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h3 className="card-title text-sm font-semibold text-base-content/70">Name</h3>
|
||||
</div>
|
||||
<p className="text-lg font-medium text-base-content">{user?.name || 'N/A'}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="font-semibold">Telegram Channel:</label>
|
||||
<p>{user?.telegramChannel || 'Not set'}</p>
|
||||
<div className="flex gap-2 mt-2">
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => setShowTelegramModal(true)}
|
||||
>
|
||||
Update Telegram Channel
|
||||
</button>
|
||||
{user?.telegramChannel && (
|
||||
{/* Agent Name Card */}
|
||||
<div className="card bg-base-100 shadow-md">
|
||||
<div className="card-body">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h3 className="card-title text-sm font-semibold text-base-content/70">Agent Name</h3>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className={`text-lg font-medium flex-1 ${user?.agentName ? 'text-base-content' : 'text-base-content/50'}`}>
|
||||
{user?.agentName || 'Not set'}
|
||||
</p>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={testTelegramChannel}
|
||||
className="btn btn-primary btn-sm"
|
||||
onClick={() => setShowUpdateModal(true)}
|
||||
>
|
||||
Test Channel
|
||||
Update
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Avatar Card */}
|
||||
<div className="card bg-base-100 shadow-md">
|
||||
<div className="card-body">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="card-title text-sm font-semibold text-base-content/70">Avatar</h3>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
{user?.avatarUrl ? (
|
||||
<div className="avatar">
|
||||
<div className="w-20 h-20 rounded-full ring ring-primary ring-offset-base-100 ring-offset-2">
|
||||
<img
|
||||
src={user.avatarUrl}
|
||||
alt="User avatar"
|
||||
className="rounded-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="avatar placeholder">
|
||||
<div className="w-20 h-20 rounded-full bg-neutral text-neutral-content ring ring-primary ring-offset-base-100 ring-offset-2">
|
||||
<span className="text-3xl font-bold">{user?.name?.[0]?.toUpperCase() || '?'}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
className="btn btn-primary btn-sm"
|
||||
onClick={() => setShowAvatarModal(true)}
|
||||
>
|
||||
Update Avatar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Telegram Channel Card */}
|
||||
<div className="card bg-base-100 shadow-md">
|
||||
<div className="card-body">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h3 className="card-title text-sm font-semibold text-base-content/70">Telegram Channel</h3>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-2 mb-3">
|
||||
<p className={`text-lg font-medium flex-1 ${user?.telegramChannel ? 'text-base-content' : 'text-base-content/50'}`}>
|
||||
{user?.telegramChannel || 'Not set'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
className="btn btn-primary btn-sm flex-1"
|
||||
onClick={() => setShowTelegramModal(true)}
|
||||
>
|
||||
Update Channel
|
||||
</button>
|
||||
{user?.telegramChannel && (
|
||||
<button
|
||||
className="btn btn-secondary btn-sm"
|
||||
onClick={testTelegramChannel}
|
||||
>
|
||||
Test
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,6 @@ import {Tabs} from '../../components/mollecules'
|
||||
|
||||
import MoneyManagementSettings from './moneymanagement/moneyManagement'
|
||||
import Theme from './theme'
|
||||
import DefaultConfig from './defaultConfig/defaultConfig'
|
||||
import UserInfoSettings from './UserInfoSettings'
|
||||
import AccountFee from './accountFee/accountFee'
|
||||
|
||||
@@ -36,11 +35,6 @@ const tabs: TabsType = [
|
||||
index: 4,
|
||||
label: 'Theme',
|
||||
},
|
||||
{
|
||||
Component: DefaultConfig,
|
||||
index: 5,
|
||||
label: 'Quick Start Config',
|
||||
},
|
||||
]
|
||||
|
||||
const Settings: React.FC = () => {
|
||||
|
||||
Reference in New Issue
Block a user