refactor(ui): standardize operations table badges
This commit is contained in:
parent
c51544aa29
commit
3951767ab1
@ -7,7 +7,7 @@ import {
|
||||
import { useCanonicalLifecycle } from '../hooks/useCanonicalLifecycle';
|
||||
import { fetchTradeHistory } from '../lib/tradeHistoryApi';
|
||||
import { fetchPositionsBootstrap } from '../lib/positionsApi';
|
||||
import { Button, Input, Select } from '../components/ui/Primitives';
|
||||
import { Badge, Button, Input, Select } from '../components/ui/Primitives';
|
||||
|
||||
|
||||
interface TradeRecord {
|
||||
@ -53,6 +53,9 @@ interface HistoryTabProps {
|
||||
botState?: any;
|
||||
}
|
||||
|
||||
const historySourceBadgeVariant = (source?: 'BOT' | 'MANUAL') => source === 'BOT' ? 'accent' : 'warning';
|
||||
const historySideBadgeVariant = (side: string) => side === 'BUY' ? 'success' : side === 'SELL' ? 'danger' : 'neutral';
|
||||
|
||||
export interface HistoryDateBounds {
|
||||
from: number | null;
|
||||
to: number | null;
|
||||
@ -587,11 +590,11 @@ export const HistoryTab = ({ botState }: HistoryTabProps) => {
|
||||
className={`${isLoss ? 'bg-red-500/[0.06] border-l-2 border-l-red-500/50' : ''} hover:bg-white/[0.02] transition-colors`}
|
||||
>
|
||||
<td className="px-6 py-4">
|
||||
<span className={`px-2 py-0.5 rounded text-[10px] font-black tracking-tighter ${t.source === 'BOT' ? 'bg-purple-500/20 text-purple-400 border border-purple-500/20' : 'bg-orange-500/20 text-orange-400 border border-orange-500/20'}`}>
|
||||
<Badge variant={historySourceBadgeVariant(t.source)} size="sm">
|
||||
{t.source === 'BOT'
|
||||
? (t.profile_id ? (profiles.find(p => p.id === t.profile_id)?.name || 'BOT') : 'BOT')
|
||||
: 'MANUAL'}
|
||||
</span>
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-[10px] text-gray-500 font-mono">
|
||||
{(t.timestamp || t.created_at) ? new Date(t.timestamp || t.created_at!).toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }) : '-'}
|
||||
@ -604,9 +607,9 @@ export const HistoryTab = ({ botState }: HistoryTabProps) => {
|
||||
</td>
|
||||
<td className="px-6 py-4 font-mono font-bold text-white">{t.symbol}</td>
|
||||
<td className="px-6 py-4">
|
||||
<span className={`text-[10px] font-black ${t.side === 'BUY' ? 'text-green-400' : t.side === 'SELL' ? 'text-red-400' : 'text-gray-400'}`}>
|
||||
<Badge variant={historySideBadgeVariant(t.side)} size="sm">
|
||||
{t.side}
|
||||
</span>
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-right text-xs font-mono text-cyan-300">
|
||||
{capitalUsed !== null
|
||||
@ -630,9 +633,9 @@ export const HistoryTab = ({ botState }: HistoryTabProps) => {
|
||||
${pnlValue.toFixed(2)}
|
||||
</div>
|
||||
{isLoss && (
|
||||
<div className="mt-1 inline-flex items-center px-1.5 py-0.5 rounded border border-red-500/40 bg-red-500/20 text-[8px] font-black uppercase tracking-wider text-red-200">
|
||||
<Badge variant="danger" size="sm" className="mt-1 text-[10px]">
|
||||
Loss Alert
|
||||
</div>
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@ -8,7 +8,7 @@ import { createRequestId } from '../../../shared/request-id.js';
|
||||
import { Layers, ListFilter, Link2, GitBranch, AlertTriangle, Lock, RefreshCw, CheckCircle, XCircle } from 'lucide-react';
|
||||
import { useCanonicalLifecycle } from '../hooks/useCanonicalLifecycle';
|
||||
import { fetchPositionsBootstrap } from '../lib/positionsApi';
|
||||
import { Button, Input, Select } from '../components/ui/Primitives';
|
||||
import { Badge, Button, Input, Select } from '../components/ui/Primitives';
|
||||
|
||||
interface PositionsTabProps {
|
||||
botState: BotState;
|
||||
@ -274,7 +274,7 @@ export const normalizeOrder = (record: RawOrderRecord, fallbackSource?: OrderSou
|
||||
};
|
||||
};
|
||||
|
||||
const SourceTruthVariants: Record<'exchange' | 'db' | 'reconciled' | 'unknown', { label: string; className: string }> = {
|
||||
const SourceTruthVariants: Record<'exchange' | 'db' | 'reconciled' | 'unknown', { label: string; className: string }> = {
|
||||
exchange: { label: 'Exchange', className: 'truth-pill exchange' },
|
||||
db: { label: 'DB', className: 'truth-pill db' },
|
||||
reconciled: { label: 'Reconciled', className: 'truth-pill reconciled' },
|
||||
@ -293,6 +293,25 @@ const getTruthSourceForPosition = (entryOrder?: NormalizedOrder, canonicalAvaila
|
||||
return SourceTruthVariants.reconciled;
|
||||
};
|
||||
|
||||
const sourceBadgeVariant = (source: 'BOT' | 'MANUAL') => source === 'BOT' ? 'accent' : 'warning';
|
||||
const sideBadgeVariant = (side: 'BUY' | 'SELL') => side === 'BUY' ? 'success' : 'danger';
|
||||
const actionBadgeVariant = (action: OrderAction) => action === 'ENTRY' ? 'info' : 'warning';
|
||||
const orderStatusBadgeVariant = (status: string, stale = false) => {
|
||||
if (stale) return 'warning';
|
||||
if (status === 'filled') return 'success';
|
||||
if (status === 'expired') return 'warning';
|
||||
if (status === 'unknown') return 'neutral';
|
||||
if (status.includes('reject') || status.includes('fail') || status.includes('cancel')) return 'danger';
|
||||
if (status.includes('pending') || status.includes('new')) return 'info';
|
||||
return 'neutral';
|
||||
};
|
||||
const lifecycleStateBadgeVariant = (state: string) => {
|
||||
if (state === 'CLOSED') return 'success';
|
||||
if (state === 'PARTIAL_EXIT' || state === 'EXIT_PENDING') return 'warning';
|
||||
if (state === 'ORPHAN_EXIT') return 'danger';
|
||||
return 'info';
|
||||
};
|
||||
|
||||
const getTruthSourceForOrder = (order: NormalizedOrder, historyKeys: Set<string>, canonicalAvailable: boolean = true) => {
|
||||
const status = order.status || '';
|
||||
if (status.includes('pending') || status.includes('new') || status.includes('accepted')) {
|
||||
@ -1436,11 +1455,11 @@ export const PositionsTab = ({ botState, onManageHolding }: PositionsTabProps) =
|
||||
|
||||
return (
|
||||
<tr key={pos.id} className="hover:bg-white/[0.02] transition-colors">
|
||||
<td className="px-6 py-4">
|
||||
<span className={`px-2 py-0.5 rounded text-[10px] font-black tracking-tighter ${pos.source === 'BOT' ? 'bg-purple-500/20 text-purple-400 border border-purple-500/20' : 'bg-orange-500/20 text-orange-400 border border-orange-500/20'}`}>
|
||||
{pos.source === 'BOT' ? (pos.profileName || profiles.find(pr => pr.id === pos.profileId)?.name || 'BOT') : 'MANUAL'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<Badge variant={sourceBadgeVariant(pos.source)} size="sm">
|
||||
{pos.source === 'BOT' ? (pos.profileName || profiles.find(pr => pr.id === pos.profileId)?.name || 'BOT') : 'MANUAL'}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<span className={positionTruth.className}>{positionTruth.label}</span>
|
||||
</td>
|
||||
@ -1458,16 +1477,12 @@ export const PositionsTab = ({ botState, onManageHolding }: PositionsTabProps) =
|
||||
</td>
|
||||
<td className="px-6 py-4 font-mono font-bold text-white">{pos.symbol}</td>
|
||||
<td className="px-6 py-4">
|
||||
<span className={`text-[10px] font-black ${pos.side === 'BUY' ? 'text-green-400' : 'text-red-400'}`}>{pos.side}</span>
|
||||
<Badge variant={sideBadgeVariant(pos.side)} size="sm">{pos.side}</Badge>
|
||||
{pos.planMode ? (
|
||||
<div className="mt-1 flex flex-wrap gap-1">
|
||||
<span className={`px-1.5 py-0.5 rounded text-[9px] font-black uppercase tracking-wider border ${
|
||||
pos.planMode === 'long_term'
|
||||
? 'bg-amber-500/10 text-amber-300 border-amber-500/20'
|
||||
: 'bg-sky-500/10 text-sky-300 border-sky-500/20'
|
||||
}`}>
|
||||
<Badge variant={pos.planMode === 'long_term' ? 'warning' : 'info'} size="sm" className="text-[10px]">
|
||||
{pos.planMode === 'long_term' ? 'Long-term hold' : 'Short-term managed'}
|
||||
</span>
|
||||
</Badge>
|
||||
</div>
|
||||
) : null}
|
||||
</td>
|
||||
@ -1479,14 +1494,14 @@ export const PositionsTab = ({ botState, onManageHolding }: PositionsTabProps) =
|
||||
{currentPrice !== null ? `$${currentPrice.toLocaleString()}` : '-'}
|
||||
<div className="mt-1 flex flex-wrap gap-1">
|
||||
{slBreached && (
|
||||
<span className="px-1.5 py-0.5 rounded text-[9px] font-black uppercase tracking-wider bg-red-500/20 text-red-300 border border-red-500/20">
|
||||
<Badge variant="danger" size="sm" className="text-[10px]">
|
||||
SL breached
|
||||
</span>
|
||||
</Badge>
|
||||
)}
|
||||
{!slBreached && tpHit && (
|
||||
<span className="px-1.5 py-0.5 rounded text-[9px] font-black uppercase tracking-wider bg-green-500/20 text-green-300 border border-green-500/20">
|
||||
<Badge variant="success" size="sm" className="text-[10px]">
|
||||
TP hit
|
||||
</span>
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
@ -1704,9 +1719,9 @@ export const PositionsTab = ({ botState, onManageHolding }: PositionsTabProps) =
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-4">
|
||||
<span className={`px-2 py-0.5 rounded text-[10px] font-black tracking-tighter ${order.source === 'BOT' ? 'bg-purple-500/20 text-purple-400 border border-purple-500/20' : 'bg-orange-500/20 text-orange-400 border border-orange-500/20'}`}>
|
||||
<Badge variant={sourceBadgeVariant(order.source)} size="sm">
|
||||
{order.profileId ? (profiles.find(pr => pr.id === order.profileId)?.name || 'BOT') : 'MANUAL'}
|
||||
</span>
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-4 py-4">
|
||||
<span className={truthSource.className}>{truthSource.label}</span>
|
||||
@ -1715,21 +1730,18 @@ export const PositionsTab = ({ botState, onManageHolding }: PositionsTabProps) =
|
||||
{order.timestamp ? new Date(order.timestamp).toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }) : '-'}
|
||||
</td>
|
||||
<td className="px-4 py-4 font-mono font-bold text-white text-xs">{order.symbol}</td>
|
||||
<td className="px-4 py-4">
|
||||
{resolvedAction ? (
|
||||
<span className={`flex items-center gap-1 px-2 py-0.5 rounded text-[9px] font-bold uppercase tracking-wider w-fit ${isEntry
|
||||
? 'bg-blue-500/10 text-blue-400 border border-blue-500/20'
|
||||
: 'bg-amber-500/10 text-amber-400 border border-amber-500/20'
|
||||
}`}>
|
||||
{isEntry ? 'ENTRY' : 'EXIT'}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-[10px] text-gray-500 uppercase">{order.type}</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-4">
|
||||
<span className={`text-[10px] font-black ${order.side === 'BUY' ? 'text-green-400' : 'text-red-400'}`}>{order.side}</span>
|
||||
</td>
|
||||
<td className="px-4 py-4">
|
||||
{resolvedAction ? (
|
||||
<Badge variant={actionBadgeVariant(resolvedAction)} size="sm">
|
||||
{isEntry ? 'ENTRY' : 'EXIT'}
|
||||
</Badge>
|
||||
) : (
|
||||
<span className="text-[10px] text-gray-500 uppercase">{order.type}</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-4">
|
||||
<Badge variant={sideBadgeVariant(order.side)} size="sm">{order.side}</Badge>
|
||||
</td>
|
||||
<td className="px-4 py-4 text-xs font-mono text-gray-300">{Number(order.qty || 0).toFixed(4)}</td>
|
||||
<td className="px-4 py-4 text-xs font-mono text-gray-400">${Number(order.price).toLocaleString()}</td>
|
||||
<td className="px-4 py-4 text-right">
|
||||
@ -1746,30 +1758,21 @@ export const PositionsTab = ({ botState, onManageHolding }: PositionsTabProps) =
|
||||
const orderAge = order.timestamp ? Date.now() - order.timestamp : 0;
|
||||
const isStale = isPendingNew && orderAge > 5 * 60 * 1000;
|
||||
|
||||
let badgeClass = 'bg-white/10 text-gray-400';
|
||||
let tooltip = '';
|
||||
|
||||
if (order.status === 'filled') {
|
||||
badgeClass = 'bg-green-500/20 text-green-400 border border-green-500/20';
|
||||
} else if (isStale) {
|
||||
badgeClass = 'bg-yellow-500/20 text-yellow-400 border border-yellow-500/20';
|
||||
tooltip = 'Order pending for >5 min - sync in progress';
|
||||
} else if (isExpired) {
|
||||
badgeClass = 'bg-orange-500/20 text-orange-400 border border-orange-500/20';
|
||||
tooltip = 'Order not found on exchange - likely never executed';
|
||||
} else if (isUnknown) {
|
||||
badgeClass = 'bg-gray-500/20 text-gray-400 border border-gray-500/20';
|
||||
tooltip = 'Order status could not be verified';
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
<span
|
||||
className={`px-2 py-0.5 rounded text-[9px] font-black uppercase tracking-tighter ${badgeClass}`}
|
||||
title={tooltip}
|
||||
>
|
||||
{order.status}
|
||||
</span>
|
||||
let tooltip = '';
|
||||
|
||||
if (isStale) {
|
||||
tooltip = 'Order pending for >5 min - sync in progress';
|
||||
} else if (isExpired) {
|
||||
tooltip = 'Order not found on exchange - likely never executed';
|
||||
} else if (isUnknown) {
|
||||
tooltip = 'Order status could not be verified';
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
<Badge variant={orderStatusBadgeVariant(order.status, isStale)} size="sm" title={tooltip}>
|
||||
{order.status}
|
||||
</Badge>
|
||||
{isStale && (
|
||||
<span className="text-[8px] text-yellow-400" title="Order may be stale - sync in progress">!</span>
|
||||
)}
|
||||
@ -1940,14 +1943,11 @@ export const PositionsTab = ({ botState, onManageHolding }: PositionsTabProps) =
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-4">
|
||||
<span className={`px-2 py-0.5 rounded text-[10px] font-black tracking-tighter ${trace.source === 'BOT'
|
||||
? 'bg-purple-500/20 text-purple-400 border border-purple-500/20'
|
||||
: 'bg-orange-500/20 text-orange-400 border border-orange-500/20'
|
||||
}`}>
|
||||
{trace.profileName}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-4">
|
||||
<Badge variant={sourceBadgeVariant(trace.source)} size="sm">
|
||||
{trace.profileName}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-4 py-4 text-xs font-mono font-bold text-white">
|
||||
{trace.symbol}
|
||||
</td>
|
||||
@ -1955,14 +1955,11 @@ export const PositionsTab = ({ botState, onManageHolding }: PositionsTabProps) =
|
||||
<div className="space-y-1">
|
||||
{trace.orderedEvents.map((event) => {
|
||||
const actionLabel: OrderAction = event.action === 'EXIT' ? 'EXIT' : 'ENTRY';
|
||||
const actionClass = actionLabel === 'ENTRY'
|
||||
? 'bg-blue-500/10 text-blue-300 border border-blue-500/20'
|
||||
: 'bg-amber-500/10 text-amber-300 border border-amber-500/20';
|
||||
return (
|
||||
<div key={`${trace.tradeId}-${event.id}-${event.timestamp}`} className="flex flex-wrap items-center gap-2 text-[9px]">
|
||||
<span className={`px-1.5 py-0.5 rounded font-bold ${actionClass}`}>
|
||||
{actionLabel}
|
||||
</span>
|
||||
return (
|
||||
<div key={`${trace.tradeId}-${event.id}-${event.timestamp}`} className="flex flex-wrap items-center gap-2 text-[9px]">
|
||||
<Badge variant={actionBadgeVariant(actionLabel)} size="sm">
|
||||
{actionLabel}
|
||||
</Badge>
|
||||
<span className="text-zinc-500 font-mono">
|
||||
{event.timestamp ? new Date(event.timestamp).toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }) : '-'}
|
||||
</span>
|
||||
@ -1986,18 +1983,9 @@ export const PositionsTab = ({ botState, onManageHolding }: PositionsTabProps) =
|
||||
</td>
|
||||
<td className="px-4 py-4 text-right">
|
||||
<div className="space-y-1">
|
||||
<span className={`px-2 py-0.5 rounded text-[9px] font-black uppercase tracking-wider ${trace.state === 'CLOSED'
|
||||
? 'bg-green-500/20 text-green-300 border border-green-500/20'
|
||||
: trace.state === 'PARTIAL_EXIT'
|
||||
? 'bg-amber-500/20 text-amber-300 border border-amber-500/20'
|
||||
: trace.state === 'ORPHAN_EXIT'
|
||||
? 'bg-red-500/20 text-red-300 border border-red-500/20'
|
||||
: trace.state === 'EXIT_PENDING'
|
||||
? 'bg-yellow-500/20 text-yellow-300 border border-yellow-500/20'
|
||||
: 'bg-blue-500/20 text-blue-300 border border-blue-500/20'
|
||||
}`}>
|
||||
{trace.state}
|
||||
</span>
|
||||
<Badge variant={lifecycleStateBadgeVariant(trace.state)} size="sm">
|
||||
{trace.state}
|
||||
</Badge>
|
||||
<div className="text-[9px] text-zinc-500 leading-tight text-right max-w-[240px] ml-auto">
|
||||
{trace.stateReason}
|
||||
</div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user