292 lines
9.5 KiB
TypeScript
292 lines
9.5 KiB
TypeScript
import { DataTable } from '@/components/ui';
|
|
import { PlusIcon, XMarkIcon } from '@heroicons/react/24/outline';
|
|
import { ColumnDef } from '@tanstack/react-table';
|
|
import { useCallback, useMemo, useState } from 'react';
|
|
import { useExchanges } from '../hooks/useExchanges';
|
|
import { Exchange, SourceMapping } from '../types';
|
|
import { AddSourceDialog } from './AddSourceDialog';
|
|
|
|
export function ExchangesTable() {
|
|
const { exchanges, loading, error, updateExchange, addSource, removeSource } = useExchanges();
|
|
const [editingCell, setEditingCell] = useState<{ id: string; field: string } | null>(null);
|
|
const [editValue, setEditValue] = useState('');
|
|
const [addSourceDialog, setAddSourceDialog] = useState<{
|
|
id: string;
|
|
exchangeName: string;
|
|
} | null>(null);
|
|
|
|
const handleCellEdit = useCallback(
|
|
async (id: string, field: string, value: string) => {
|
|
if (field === 'shortName') {
|
|
await updateExchange(id, { shortName: value });
|
|
}
|
|
setEditingCell(null);
|
|
setEditValue('');
|
|
},
|
|
[updateExchange]
|
|
);
|
|
|
|
const handleToggleActive = useCallback(
|
|
async (id: string, currentStatus: boolean) => {
|
|
await updateExchange(id, { active: !currentStatus });
|
|
},
|
|
[updateExchange]
|
|
);
|
|
|
|
const handleAddSource = useCallback(async (id: string, exchangeName: string) => {
|
|
setAddSourceDialog({ id, exchangeName });
|
|
}, []);
|
|
|
|
const handleRemoveSource = useCallback(
|
|
async (exchangeId: string, sourceName: string) => {
|
|
if (confirm(`Are you sure you want to remove the ${sourceName} source?`)) {
|
|
await removeSource(exchangeId, sourceName);
|
|
}
|
|
},
|
|
[removeSource]
|
|
);
|
|
|
|
const columns = useMemo<ColumnDef<Exchange>[]>(() => {
|
|
return [
|
|
{
|
|
id: 'masterExchangeId',
|
|
header: 'Master ID',
|
|
accessorKey: 'masterExchangeId',
|
|
size: 50,
|
|
enableResizing: false,
|
|
cell: ({ getValue, cell }) => (
|
|
<span
|
|
className="font-mono text-primary-400 text-xs"
|
|
style={{ width: cell.column.getSize() }}
|
|
>
|
|
{getValue() as string}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
id: 'shortName',
|
|
header: 'Short Name',
|
|
accessorKey: 'shortName',
|
|
size: 50,
|
|
enableResizing: false,
|
|
cell: ({ getValue, row, cell }) => {
|
|
const isEditing =
|
|
editingCell?.id === row.original._id && editingCell?.field === 'shortName';
|
|
|
|
if (isEditing) {
|
|
return (
|
|
<input
|
|
type="text"
|
|
style={{ width: cell.column.getSize() }}
|
|
value={editValue}
|
|
onChange={e => setEditValue(e.target.value)}
|
|
onBlur={() => handleCellEdit(row.original._id, 'shortName', editValue)}
|
|
onKeyDown={e => {
|
|
if (e.key === 'Enter') {
|
|
handleCellEdit(row.original._id, 'shortName', editValue);
|
|
} else if (e.key === 'Escape') {
|
|
setEditingCell(null);
|
|
setEditValue('');
|
|
}
|
|
}}
|
|
className="w-full bg-surface border border-border rounded px-2 py-1 text-sm"
|
|
autoFocus
|
|
/>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className="cursor-pointer hover:bg-surface-secondary rounded px-2 py-1 transition-colors text-sm"
|
|
onClick={() => {
|
|
setEditingCell({ id: row.original._id, field: 'shortName' });
|
|
setEditValue(getValue() as string);
|
|
}}
|
|
>
|
|
{getValue() as string}
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
id: 'officialName',
|
|
header: 'Official Name',
|
|
accessorKey: 'officialName',
|
|
size: 150,
|
|
maxSize: 150,
|
|
enableResizing: true,
|
|
cell: ({ getValue, cell }) => (
|
|
<span
|
|
className="text-text-primary text-sm truncate block"
|
|
title={getValue() as string}
|
|
style={{ width: cell.column.getSize() }}
|
|
>
|
|
{getValue() as string}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
id: 'country',
|
|
header: 'Country',
|
|
accessorKey: 'country',
|
|
size: 40,
|
|
maxSize: 40,
|
|
cell: ({ getValue }) => (
|
|
<span className="text-text-secondary text-sm">{getValue() as string}</span>
|
|
),
|
|
},
|
|
{
|
|
id: 'currency',
|
|
header: 'Currency',
|
|
accessorKey: 'currency',
|
|
size: 40,
|
|
cell: ({ getValue, cell }) => (
|
|
<span
|
|
className="font-mono text-text-secondary text-sm"
|
|
style={{ width: cell.column.getSize() }}
|
|
>
|
|
{getValue() as string}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
id: 'active',
|
|
header: 'Active',
|
|
accessorKey: 'active',
|
|
size: 80,
|
|
maxSize: 80,
|
|
cell: ({ getValue, row, cell }) => {
|
|
const isActive = getValue() as boolean;
|
|
return (
|
|
<label
|
|
className="relative inline-flex items-center cursor-pointer"
|
|
style={{ width: cell.column.getSize() }}
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
checked={isActive}
|
|
onChange={() => handleToggleActive(row.original._id, isActive)}
|
|
className="sr-only peer"
|
|
/>
|
|
<div className="w-9 h-5 bg-surface-secondary peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-primary-500 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-primary-500"></div>
|
|
</label>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
id: 'sources',
|
|
header: 'Sources',
|
|
accessorKey: 'sourceMappings',
|
|
minSize: 400,
|
|
maxSize: 400,
|
|
size: 400,
|
|
enableResizing: true,
|
|
cell: ({ getValue, row, cell }) => {
|
|
const sourceMappings = getValue() as Record<string, SourceMapping>;
|
|
const sources = Object.keys(sourceMappings);
|
|
|
|
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"
|
|
>
|
|
<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"
|
|
>
|
|
<PlusIcon className="h-3 w-3" />
|
|
Add Source
|
|
</button>
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
id: 'updated_at',
|
|
header: 'Last Updated',
|
|
accessorKey: 'updated_at',
|
|
size: 150,
|
|
maxSize: 150,
|
|
cell: ({ getValue }) => (
|
|
<span className="text-xs text-text-muted">
|
|
{new Date(getValue() as string).toLocaleDateString()}
|
|
</span>
|
|
),
|
|
},
|
|
];
|
|
}, [
|
|
editingCell,
|
|
editValue,
|
|
handleCellEdit,
|
|
handleRemoveSource,
|
|
handleToggleActive,
|
|
handleAddSource,
|
|
]);
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="bg-danger/10 border border-danger/20 rounded-lg p-4">
|
|
<h3 className="text-danger font-medium mb-2">Error Loading Exchanges</h3>
|
|
<p className="text-text-secondary text-sm">{error}</p>
|
|
<p className="text-text-muted text-xs mt-2">
|
|
Make sure the data-service is running on localhost:2001
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h2 className="text-lg font-bold text-text-primary">Exchanges Management</h2>
|
|
<p className="text-sm text-text-secondary">
|
|
Manage exchange configurations and source mappings
|
|
</p>
|
|
</div>
|
|
<div className="text-sm text-text-muted">{exchanges?.length || 0} exchanges loaded</div>
|
|
</div>
|
|
|
|
<DataTable
|
|
data={exchanges || []}
|
|
columns={columns}
|
|
loading={loading}
|
|
className="rounded-lg border border-border"
|
|
/>
|
|
|
|
<div className="mt-2 text-xs text-text-muted">
|
|
Debug: Data length: {exchanges?.length || 0}, Loading: {loading.toString()}, Error:{' '}
|
|
{error || 'none'}
|
|
</div>
|
|
|
|
{addSourceDialog && (
|
|
<AddSourceDialog
|
|
isOpen={true}
|
|
exchangeId={addSourceDialog.id}
|
|
exchangeName={addSourceDialog.exchangeName}
|
|
onClose={() => setAddSourceDialog(null)}
|
|
onAddSource={async (sourceRequest: {
|
|
source: string;
|
|
mapping: { id: string; name: string; code: string; aliases: string[] };
|
|
}) => {
|
|
const success = await addSource(addSourceDialog.id, sourceRequest);
|
|
if (success) {
|
|
setAddSourceDialog(null);
|
|
}
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|