finished exchanges api connections

This commit is contained in:
Boki 2025-06-16 09:24:17 -04:00
parent d7780e9684
commit e8fbe76f2e
6 changed files with 306 additions and 23 deletions

View file

@ -1,15 +1,115 @@
import { ArrowPathIcon, CheckCircleIcon, ExclamationTriangleIcon, XMarkIcon } from '@heroicons/react/24/outline';
import { useEffect, useState } from 'react';
import { ExchangesTable } from './components/ExchangesTable';
import { useExchanges } from './hooks/useExchanges';
export function ExchangesPage() {
const { syncExchanges } = useExchanges();
const [syncing, setSyncing] = useState(false);
const [syncStatus, setSyncStatus] = useState<{
type: 'success' | 'error' | null;
message: string;
}>({ type: null, message: '' });
// Auto-dismiss success messages after 5 seconds
useEffect(() => {
if (syncStatus.type === 'success') {
const timer = setTimeout(() => {
setSyncStatus({ type: null, message: '' });
}, 5000);
return () => clearTimeout(timer);
}
}, [syncStatus.type]);
const handleSync = async () => {
setSyncing(true);
setSyncStatus({ type: null, message: '' });
try {
const result = await syncExchanges();
if (result) {
setSyncStatus({
type: 'success',
message: `Exchange sync completed successfully! Job ID: ${result.jobId || 'Unknown'}`,
});
} else {
setSyncStatus({
type: 'error',
message: 'Sync failed - no result returned from server',
});
}
} catch (error) {
setSyncStatus({
type: 'error',
message: error instanceof Error ? error.message : 'Sync failed with unknown error',
});
} finally {
setSyncing(false);
}
};
return (
<div className="space-y-6">
<div>
<h1 className="text-lg font-bold text-text-primary mb-2">Exchange Management</h1>
<p className="text-text-secondary text-sm">
Configure and manage master exchanges with their data sources and providers.
</p>
<div className="flex items-center justify-between">
<div>
<h1 className="text-lg font-bold text-text-primary mb-2">Exchange Management</h1>
<p className="text-text-secondary text-sm">
Configure and manage master exchanges with their data sources and providers.
</p>
</div>
<button
onClick={handleSync}
disabled={syncing}
className="inline-flex items-center gap-2 px-4 py-2 bg-primary-500 text-white rounded-lg hover:bg-primary-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
<ArrowPathIcon className={`h-4 w-4 ${syncing ? 'animate-spin' : ''}`} />
{syncing ? 'Syncing...' : 'Sync Exchanges'}
</button>
</div>
{syncing && (
<div className="bg-primary-50 border border-primary-200 rounded-lg p-4">
<div className="flex items-center gap-2">
<ArrowPathIcon className="h-4 w-4 text-primary-500 animate-spin" />
<span className="text-primary-700 text-sm">
Syncing exchanges from Interactive Brokers data...
</span>
</div>
</div>
)}
{syncStatus.type && (
<div className="fixed bottom-4 right-4 z-50 max-w-sm">
<div className="bg-surface-secondary rounded-lg border border-border p-4 shadow-xl transform transition-all duration-300 ease-out">
<div className="flex items-start gap-3">
{syncStatus.type === 'success' ? (
<CheckCircleIcon className="h-5 w-5 text-success flex-shrink-0 mt-0.5" />
) : (
<ExclamationTriangleIcon className="h-5 w-5 text-danger flex-shrink-0 mt-0.5" />
)}
<div className="flex-1 min-w-0">
<p
className={`text-sm font-medium ${
syncStatus.type === 'success' ? 'text-success' : 'text-danger'
}`}
>
{syncStatus.type === 'success' ? 'Sync Completed' : 'Sync Failed'}
</p>
<p className="text-xs mt-1 text-text-secondary">
{syncStatus.message}
</p>
</div>
<button
onClick={() => setSyncStatus({ type: null, message: '' })}
className="flex-shrink-0 p-1 rounded-full text-text-muted hover:text-text-primary hover:bg-surface transition-colors"
>
<XMarkIcon className="h-4 w-4" />
</button>
</div>
</div>
</div>
)}
<ExchangesTable />
</div>
);

View file

@ -15,10 +15,10 @@ export function AddSourceDialog({
isOpen,
onClose,
onAddSource,
exchangeId,
exchangeName,
}: AddSourceDialogProps) {
const [source, setSource] = useState('');
const [sourceCode, setSourceCode] = useState('');
const [id, setId] = useState('');
const [name, setName] = useState('');
const [code, setCode] = useState('');
@ -27,12 +27,13 @@ export function AddSourceDialog({
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!source || !id || !name || !code) return;
if (!source || !sourceCode || !id || !name || !code) return;
setLoading(true);
try {
await onAddSource({
source,
source_code: sourceCode,
mapping: {
id,
name,
@ -46,6 +47,7 @@ export function AddSourceDialog({
// Reset form
setSource('');
setSourceCode('');
setId('');
setName('');
setCode('');
@ -116,6 +118,20 @@ export function AddSourceDialog({
</select>
</div>
<div>
<label className="block text-sm font-medium text-text-primary mb-1">
Source Code
</label>
<input
type="text"
value={sourceCode}
onChange={e => setSourceCode(e.target.value)}
className="w-full bg-surface border border-border rounded px-3 py-2 text-text-primary focus:ring-1 focus:ring-primary-500 focus:border-primary-500"
placeholder="e.g., IB, ALP, POLY"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-text-primary mb-1">
Source ID
@ -181,7 +197,7 @@ export function AddSourceDialog({
</button>
<button
type="submit"
disabled={loading || !source || !id || !name || !code}
disabled={loading || !source || !sourceCode || !id || !name || !code}
className="px-4 py-2 bg-primary-500 text-white rounded hover:bg-primary-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{loading ? 'Adding...' : 'Add Source'}

View file

@ -187,26 +187,31 @@ export function ExchangesTable() {
return (
<div className="flex flex-wrap gap-1" style={{ width: cell.column.getSize() }}>
{sources.map(source => (
<span
key={source}
className="inline-flex items-center gap-1 px-2 py-1 bg-surface-secondary rounded text-xs"
>
{source.toUpperCase()}
<button
onClick={() => handleRemoveSource(row.original._id, source)}
className="text-danger hover:text-danger/80 transition-colors"
{sources.map(source => {
// The source key is already in format "source_sourcecode" from the storage
const displayText = source.toUpperCase();
return (
<span
key={source}
className="inline-flex items-center gap-1 px-2 py-1 bg-surface-secondary rounded text-xs"
>
<XMarkIcon className="h-3 w-3" />
</button>
</span>
))}
{displayText}
<button
onClick={() => handleRemoveSource(row.original._id, source)}
className="text-danger hover:text-danger/80 transition-colors"
>
<XMarkIcon className="h-3 w-3" />
</button>
</span>
);
})}
<button
onClick={() => handleAddSource(row.original._id, row.original.officialName)}
className="inline-flex items-center gap-1 px-2 py-1 bg-primary-500/20 text-primary-400 rounded text-xs hover:bg-primary-500/30 transition-colors"
className="inline-flex items-center justify-center w-6 h-6 bg-primary-500/20 text-primary-400 rounded text-xs hover:bg-primary-500/30 transition-colors"
title="Add Source"
>
<PlusIcon className="h-3 w-3" />
Add Source
</button>
</div>
);
@ -278,6 +283,7 @@ export function ExchangesTable() {
onClose={() => setAddSourceDialog(null)}
onAddSource={async (sourceRequest: {
source: string;
source_code: string;
mapping: { id: string; name: string; code: string; aliases: string[] };
}) => {
const success = await addSource(addSourceDialog.id, sourceRequest);

View file

@ -116,6 +116,28 @@ export function useExchanges() {
[fetchExchanges]
);
const syncExchanges = useCallback(async () => {
try {
const response = await fetch(`${API_BASE_URL}/exchanges/sync`, {
method: 'POST',
});
if (!response.ok) {
throw new Error(`Failed to sync exchanges: ${response.statusText}`);
}
const result = await response.json();
// Refresh the exchanges list after sync
await fetchExchanges();
return result;
} catch (err) {
console.error('Error syncing exchanges:', err);
setError(err instanceof Error ? err.message : 'Failed to sync exchanges');
return null;
}
}, [fetchExchanges]);
useEffect(() => {
fetchExchanges();
}, [fetchExchanges]);
@ -128,5 +150,6 @@ export function useExchanges() {
updateExchange,
addSource,
removeSource,
syncExchanges,
};
}

View file

@ -36,6 +36,7 @@ export interface UpdateExchangeRequest {
export interface AddSourceRequest {
source: string;
source_code: string;
mapping: {
id: string;
name: string;