Update composite And update scenario

This commit is contained in:
2025-06-02 21:57:34 +07:00
parent 7fce1fa59e
commit 71bcaea76d
7 changed files with 244 additions and 80 deletions

View File

@@ -0,0 +1,110 @@
import React, {useEffect} from 'react'
import {SubmitHandler, useForm} from 'react-hook-form'
import {Modal} from '../../mollecules'
import type {Scenario, Strategy} from '../../../generated/ManagingApi'
import type {IScenarioFormInput} from '../../../global/type'
interface ScenarioModalProps {
showModal: boolean
onClose: () => void
onSubmit: (data: IScenarioFormInput) => Promise<void>
strategies: Strategy[]
scenario?: Scenario | null // For update mode
isUpdate?: boolean
}
const ScenarioModal: React.FC<ScenarioModalProps> = ({
showModal,
onClose,
onSubmit,
strategies,
scenario = null,
isUpdate = false
}) => {
const { register, handleSubmit, reset, setValue } = useForm<IScenarioFormInput>()
// Reset form when modal opens/closes or scenario changes
useEffect(() => {
if (showModal) {
if (isUpdate && scenario) {
// Pre-populate form for update
setValue('name', scenario.name || '')
setValue('loopbackPeriod', scenario.loopbackPeriod || 0)
setValue('strategies', scenario.strategies?.map(s => s.name || '') || [])
} else {
// Reset form for create
reset()
}
}
}, [showModal, isUpdate, scenario, setValue, reset])
const handleFormSubmit: SubmitHandler<IScenarioFormInput> = async (data) => {
onClose()
await onSubmit(data)
}
const titleHeader = isUpdate ? 'Update Scenario' : 'Scenario Builder'
const submitButtonText = isUpdate ? 'Update' : 'Build'
return (
<Modal
onClose={onClose}
onSubmit={handleSubmit(handleFormSubmit)}
showModal={showModal}
titleHeader={titleHeader}
>
<div className="form-control mb-5">
<div className="input-group">
<label htmlFor="name" className="label mr-6">
Name
</label>
<input
className="bg-inherit w-full max-w-xs"
{...register('name')}
disabled={isUpdate} // Disable name editing in update mode
/>
</div>
</div>
<div className="form-control">
<div className="input-group">
<label htmlFor="strategies" className="label mr-6">
Strategies
</label>
<select
multiple
className="select select-bordered h-28 w-full max-w-xs"
{...register('strategies')}
>
{strategies.map((item) => (
<option key={item.name} value={item.name || ''}>
{item.signalType} - {item.name}
</option>
))}
</select>
</div>
</div>
<div className="form-control mb-5">
<div className="input-group">
<label htmlFor="loopbackPeriod" className="label mr-6">
Loopback period
</label>
<input
type="number"
className="bg-inherit w-full max-w-xs"
{...register('loopbackPeriod', { valueAsNumber: true })}
/>
</div>
</div>
<div className="modal-action">
<button type="submit" className="btn">
{submitButtonText}
</button>
</div>
</Modal>
)
}
export default ScenarioModal

View File

@@ -8,3 +8,4 @@ export { default as SpotLightBadge } from './SpotLightBadge/SpotLightBadge'
export { default as StatusBadge } from './StatusBadge/StatusBadge'
export { default as PositionsList } from './Positions/PositionList'
export { default as WorkflowCanvas } from './Workflow/workflowCanvas'
export { default as ScenarioModal } from './ScenarioModal'

View File

@@ -19,6 +19,7 @@ import type {
Scenario,
Signal,
StrategiesResultBase,
Strategy,
StrategyType,
Ticker,
Timeframe,
@@ -189,6 +190,8 @@ export type IScenarioFormInput = {
}
export type IScenarioList = {
list: Scenario[]
strategies?: Strategy[]
setScenarios?: React.Dispatch<React.SetStateAction<Scenario[]>>
}
export type IMoneyManagementList = {

View File

@@ -1,10 +1,9 @@
import React, {useEffect, useState} from 'react'
import type {SubmitHandler} from 'react-hook-form'
import {useForm} from 'react-hook-form'
import 'react-toastify/dist/ReactToastify.css'
import useApiUrlStore from '../../app/store/apiStore'
import {Modal, Toast} from '../../components/mollecules'
import {Toast} from '../../components/mollecules'
import {ScenarioModal} from '../../components/organism'
import type {Scenario, Strategy} from '../../generated/ManagingApi'
import {ScenarioClient} from '../../generated/ManagingApi'
import type {IScenarioFormInput} from '../../global/type'
@@ -15,7 +14,6 @@ const ScenarioList: React.FC = () => {
const [strategies, setStrategies] = useState<Strategy[]>([])
const [scenarios, setScenarios] = useState<Scenario[]>([])
const [showModal, setShowModal] = useState(false)
const { register, handleSubmit } = useForm<IScenarioFormInput>()
const { apiUrl } = useApiUrlStore()
const client = new ScenarioClient({}, apiUrl)
@@ -32,8 +30,7 @@ const ScenarioList: React.FC = () => {
})
}
const onSubmit: SubmitHandler<IScenarioFormInput> = async (form) => {
closeModal()
const handleSubmit = async (form: IScenarioFormInput) => {
await createScenario(form)
}
@@ -59,61 +56,14 @@ const ScenarioList: React.FC = () => {
<button className="btn" onClick={openModal}>
Create new scenario
</button>
<ScenarioTable list={scenarios} />
<Modal
{...{
onClose: closeModal,
onSubmit: handleSubmit(onSubmit),
showModal,
titleHeader: 'Scenario Builder',
}}
>
<div className="form-control mb-5">
<div className="input-group">
<label htmlFor="name" className="label mr-6">
Name
</label>
<input
className="bg-inherit w-full max-w-xs"
{...register('name')}
></input>
</div>
</div>
<div className="form-control">
<div className="input-group">
<label htmlFor="strategies" className="label mr-6">
Strategies
</label>
<select
multiple
className="select select-bordered h-28 w-full max-w-xs"
{...register('strategies')}
>
{strategies.map((item) => (
<option key={item.name} value={item.name}>
{item.signalType} - {item.name}
</option>
))}
</select>
</div>
</div>
<div className="form-control mb-5">
<div className="input-group">
<label htmlFor="name" className="label mr-6">
Loopback period
</label>
<input
className="bg-inherit w-full max-w-xs"
{...register('loopbackPeriod')}
></input>
</div>
</div>
<div className="modal-action">
<button type="submit" className="btn">
Build
</button>
</div>
</Modal>
<ScenarioTable list={scenarios} strategies={strategies} setScenarios={setScenarios} />
<ScenarioModal
showModal={showModal}
onClose={closeModal}
onSubmit={handleSubmit}
strategies={strategies}
isUpdate={false}
/>
</div>
)
}

View File

@@ -1,29 +1,72 @@
import { TrashIcon } from '@heroicons/react/solid'
import React, { useEffect, useState } from 'react'
import {PencilIcon, TrashIcon} from '@heroicons/react/solid'
import React, {useEffect, useState} from 'react'
import useApiUrlStore from '../../app/store/apiStore'
import { Toast, Table } from '../../components/mollecules'
import type { Scenario, Strategy } from '../../generated/ManagingApi'
import { ScenarioClient } from '../../generated/ManagingApi'
import type { IScenarioList } from '../../global/type'
import {Table, Toast} from '../../components/mollecules'
import {ScenarioModal} from '../../components/organism'
import type {Scenario, Strategy} from '../../generated/ManagingApi'
import {ScenarioClient} from '../../generated/ManagingApi'
import type {IScenarioFormInput, IScenarioList} from '../../global/type'
const ScenarioTable: React.FC<IScenarioList> = ({ list }) => {
const ScenarioTable: React.FC<IScenarioList> = ({ list, strategies = [], setScenarios }) => {
const [rows, setRows] = useState<Scenario[]>([])
const [showUpdateModal, setShowUpdateModal] = useState(false)
const [selectedScenario, setSelectedScenario] = useState<Scenario | null>(null)
const { apiUrl } = useApiUrlStore()
const client = new ScenarioClient({}, apiUrl)
async function deleteScenario(id: string) {
const t = new Toast('Deleting scenario')
const client = new ScenarioClient({}, apiUrl)
await client
.scenario_DeleteScenario(id)
.then(() => {
t.update('success', 'Scenario deleted')
// Refetch scenarios after deletion
if (setScenarios) {
client.scenario_GetScenarios().then((scenarios) => {
setScenarios(scenarios)
})
}
})
.catch((err) => {
t.update('error', err)
})
}
async function updateScenario(form: IScenarioFormInput) {
const t = new Toast('Updating scenario')
await client
.scenario_UpdateScenario(form.name, form.loopbackPeriod, form.strategies)
.then(() => {
t.update('success', 'Scenario updated')
// Refetch scenarios after update since the API returns FileResponse
if (setScenarios) {
client.scenario_GetScenarios().then((scenarios) => {
setScenarios(scenarios)
})
}
})
.catch((err) => {
t.update('error', err)
})
}
function openUpdateModal(scenario: Scenario) {
setSelectedScenario(scenario)
setShowUpdateModal(true)
}
function closeUpdateModal() {
setShowUpdateModal(false)
setSelectedScenario(null)
}
const handleUpdateSubmit = async (form: IScenarioFormInput) => {
await updateScenario(form)
}
const columns = React.useMemo(
() => [
{
@@ -50,18 +93,28 @@ const ScenarioTable: React.FC<IScenarioList> = ({ list }) => {
},
{
Cell: ({ cell }: any) => (
<>
<div className="flex gap-2">
<div className="tooltip" data-tip="Update scenario">
<button
data-value={cell.row.values.name}
onClick={() => openUpdateModal(cell.row.original)}
className="btn btn-ghost btn-sm"
>
<PencilIcon className="text-info w-4" />
</button>
</div>
<div className="tooltip" data-tip="Delete scenario">
<button
data-value={cell.row.values.name}
onClick={() => deleteScenario(cell.row.values.name)}
className="btn btn-ghost btn-sm"
>
<TrashIcon className="text-accent w-4"></TrashIcon>
<TrashIcon className="text-accent w-4" />
</button>
</div>
</>
</div>
),
Header: '',
Header: 'Actions',
accessor: 'id',
disableFilters: true,
},
@@ -76,6 +129,16 @@ const ScenarioTable: React.FC<IScenarioList> = ({ list }) => {
return (
<div className="flex flex-wrap">
<Table columns={columns} data={rows} showPagination={true} />
{/* Update Modal */}
<ScenarioModal
showModal={showUpdateModal}
onClose={closeUpdateModal}
onSubmit={handleUpdateSubmit}
strategies={strategies}
scenario={selectedScenario}
isUpdate={true}
/>
</div>
)
}