Files
foxhunt/web-dashboard/src/components/trading/OrderForm.tsx
jgrusewski e5bf30c494 fix(web-dashboard): switch protobuf codegen to js+dts for TS 5.9 compat
TypeScript 5.9 erasableSyntaxOnly rejects enum declarations in .ts
files (they emit runtime code). Switch protoc-gen-es from target=ts
to target=js+dts which generates .js runtime + .d.ts type declarations.

Also fix type mismatches in useApi.ts (OrderSide/OrderType enum types,
StartBacktestRequest field mapping) and MLDashboard signal defaulting.
Add web-dashboard network policy for MinIO egress + proxy ingress.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 14:32:41 +01:00

214 lines
7.6 KiB
TypeScript

import { useState, type FormEvent } from 'react';
import { useSubmitOrder } from '../../hooks/useApi';
import { OrderSide, OrderType } from '../../gen/trading_pb';
interface ValidationErrors {
symbol?: string;
quantity?: string;
price?: string;
}
export function OrderForm() {
const [symbol, setSymbol] = useState('ES.FUT');
const [side, setSide] = useState<'buy' | 'sell'>('buy');
const [orderType, setOrderType] = useState<'market' | 'limit'>('market');
const [quantity, setQuantity] = useState('1');
const [price, setPrice] = useState('');
const [validationErrors, setValidationErrors] = useState<ValidationErrors>({});
const submitOrder = useSubmitOrder();
const symbolValid = /^[A-Z0-9.]+$/i.test(symbol);
function clearFieldError(field: keyof ValidationErrors) {
setValidationErrors((prev) => {
const next = { ...prev };
delete next[field];
return next;
});
}
function handleSubmit(e: FormEvent) {
e.preventDefault();
const errors: ValidationErrors = {};
if (!symbol.trim()) {
errors.symbol = 'Symbol is required';
} else if (!/^[A-Z0-9.]+$/i.test(symbol)) {
errors.symbol = 'Symbol must be alphanumeric (dots allowed)';
}
const qty = parseFloat(quantity);
if (isNaN(qty) || qty <= 0) {
errors.quantity = 'Quantity must be greater than 0';
}
if (orderType === 'limit') {
const p = parseFloat(price);
if (isNaN(p) || p <= 0) {
errors.price = 'Price must be greater than 0 for limit orders';
}
}
setValidationErrors(errors);
if (Object.keys(errors).length > 0) return;
submitOrder.mutate({
symbol,
side: side === 'buy' ? OrderSide.BUY : OrderSide.SELL,
orderType: orderType === 'market' ? OrderType.MARKET : OrderType.LIMIT,
quantity: qty,
price: orderType === 'limit' ? parseFloat(price) : undefined,
});
}
return (
<div>
<div className="px-3 py-1.5 border-b border-[var(--color-border)]">
<span className="text-sm font-medium">New Order</span>
</div>
<form onSubmit={handleSubmit} className="p-3 space-y-3">
{/* Symbol */}
<div>
<label htmlFor="order-symbol" className="block text-xs text-[var(--color-text-secondary)] mb-1">Symbol</label>
<input
id="order-symbol"
type="text"
value={symbol}
onChange={(e) => {
setSymbol(e.target.value.toUpperCase());
clearFieldError('symbol');
}}
maxLength={20}
pattern="[A-Z0-9.]+"
className={`w-full px-2 py-1.5 text-sm rounded border bg-[var(--color-bg-primary)] text-[var(--color-text-primary)] outline-none ${
symbolValid && !validationErrors.symbol
? 'border-[var(--color-border)] focus:border-[var(--color-accent)]'
: 'border-[var(--color-red)]'
}`}
/>
{validationErrors.symbol && (
<p className="text-xs text-[var(--color-red)] mt-0.5" role="alert">{validationErrors.symbol}</p>
)}
</div>
{/* Side */}
<div>
<label className="block text-xs text-[var(--color-text-secondary)] mb-1">Side</label>
<div className="grid grid-cols-2 gap-1">
<button
type="button"
onClick={() => setSide('buy')}
aria-label="Set order side to buy"
aria-pressed={side === 'buy'}
className={`py-1.5 text-sm rounded font-medium transition-colors ${
side === 'buy'
? 'bg-[var(--color-green)] text-white'
: 'bg-[var(--color-bg-primary)] text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]'
}`}
>
BUY
</button>
<button
type="button"
onClick={() => setSide('sell')}
aria-label="Set order side to sell"
aria-pressed={side === 'sell'}
className={`py-1.5 text-sm rounded font-medium transition-colors ${
side === 'sell'
? 'bg-[var(--color-red)] text-white'
: 'bg-[var(--color-bg-primary)] text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]'
}`}
>
SELL
</button>
</div>
</div>
{/* Type */}
<div>
<label htmlFor="order-type" className="block text-xs text-[var(--color-text-secondary)] mb-1">Type</label>
<select
id="order-type"
value={orderType}
onChange={(e) => setOrderType(e.target.value as 'market' | 'limit')}
className="w-full px-2 py-1.5 text-sm rounded border border-[var(--color-border)] bg-[var(--color-bg-primary)] text-[var(--color-text-primary)] outline-none"
>
<option value="market">Market</option>
<option value="limit">Limit</option>
</select>
</div>
{/* Quantity */}
<div>
<label htmlFor="order-quantity" className="block text-xs text-[var(--color-text-secondary)] mb-1">Quantity</label>
<input
id="order-quantity"
type="number"
min="0"
step="1"
value={quantity}
onChange={(e) => {
setQuantity(e.target.value);
clearFieldError('quantity');
}}
className={`w-full px-2 py-1.5 text-sm rounded border bg-[var(--color-bg-primary)] text-[var(--color-text-primary)] outline-none ${
validationErrors.quantity
? 'border-[var(--color-red)]'
: 'border-[var(--color-border)] focus:border-[var(--color-accent)]'
}`}
/>
{validationErrors.quantity && (
<p className="text-xs text-[var(--color-red)] mt-0.5" role="alert">{validationErrors.quantity}</p>
)}
</div>
{/* Price (limit only) */}
{orderType === 'limit' && (
<div>
<label htmlFor="order-price" className="block text-xs text-[var(--color-text-secondary)] mb-1">Price</label>
<input
id="order-price"
type="number"
min="0"
step="0.01"
value={price}
onChange={(e) => {
setPrice(e.target.value);
clearFieldError('price');
}}
className={`w-full px-2 py-1.5 text-sm rounded border bg-[var(--color-bg-primary)] text-[var(--color-text-primary)] outline-none ${
validationErrors.price
? 'border-[var(--color-red)]'
: 'border-[var(--color-border)] focus:border-[var(--color-accent)]'
}`}
/>
{validationErrors.price && (
<p className="text-xs text-[var(--color-red)] mt-0.5" role="alert">{validationErrors.price}</p>
)}
</div>
)}
{/* Submit */}
<button
type="submit"
disabled={submitOrder.isPending}
className={`w-full py-2 text-sm font-medium rounded transition-colors ${
side === 'buy'
? 'bg-[var(--color-green)] hover:bg-green-600'
: 'bg-[var(--color-red)] hover:bg-red-600'
} text-white disabled:opacity-50`}
>
{submitOrder.isPending ? 'Submitting...' : `${side.toUpperCase()} ${symbol}`}
</button>
{submitOrder.isError && (
<p className="text-xs text-[var(--color-red)]" role="alert">{submitOrder.error.message}</p>
)}
</form>
</div>
);
}