This commit is contained in:
Boki 2025-06-18 08:53:51 -04:00
parent b6e2fd8c9e
commit 93542667e6
2 changed files with 127 additions and 54 deletions

View file

@ -26,16 +26,9 @@ function CellWithTooltip({ children, className }: { children: React.ReactNode; c
const textContent = element.textContent || '';
setTooltipContent(textContent);
console.log('Element dimensions:', {
scrollWidth: element.scrollWidth,
clientWidth: element.clientWidth,
textContent,
isOverflowing: element.scrollWidth > element.clientWidth
});
// Check if content is overflowing by comparing scroll width to client width
const isOverflowing = element.scrollWidth > element.clientWidth;
if (isOverflowing || textContent.length > 10) { // Temporarily show tooltip for testing
if (isOverflowing && textContent.trim().length > 0) {
setShowTooltip(true);
}
}
@ -174,9 +167,15 @@ export function DataTable<T>({
maxWidth: `${cell.column.getSize()}px`,
}}
>
<CellWithTooltip className="truncate overflow-hidden text-ellipsis whitespace-nowrap">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</CellWithTooltip>
{(cell.column.columnDef as any).disableTooltip ? (
<div className="truncate overflow-hidden text-ellipsis whitespace-nowrap">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</div>
) : (
<CellWithTooltip className="truncate overflow-hidden text-ellipsis whitespace-nowrap">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</CellWithTooltip>
)}
</td>
))}
</tr>

View file

@ -1,5 +1,5 @@
import { DataTable } from '@/components/ui';
import { PlusIcon, XMarkIcon, CheckIcon } from '@heroicons/react/24/outline';
import { PlusIcon, XMarkIcon, CheckIcon, TrashIcon } from '@heroicons/react/24/outline';
import { ColumnDef } from '@tanstack/react-table';
import { useCallback, useMemo, useState, useEffect } from 'react';
import { useExchanges } from '../hooks/useExchanges';
@ -28,8 +28,8 @@ export function ExchangesTable() {
const handleCellEdit = useCallback(
async (id: string, field: string, value: string) => {
if (field === 'name') {
await updateExchange(id, { name: value });
if (field === 'name' || field === 'code') {
await updateExchange(id, { [field]: value });
}
setEditingCell(null);
setEditValue('');
@ -48,6 +48,16 @@ export function ExchangesTable() {
setAddProviderDialog({ exchangeId, exchangeName });
}, []);
const handleDeleteExchange = useCallback(async (exchangeId: string, exchangeName: string) => {
if (confirm(`Are you sure you want to delete "${exchangeName}"? This will hide the exchange and make all its provider mappings available for remapping.`)) {
const success = await updateExchange(exchangeId, { active: false });
if (success) {
// Optionally refresh the list or show a success message
refetch();
}
}
}, [updateExchange, refetch]);
const handleToggleProviderMapping = useCallback(
async (mappingId: string, currentStatus: boolean) => {
const success = await updateProviderMapping(mappingId, { active: !currentStatus });
@ -94,28 +104,50 @@ export function ExchangesTable() {
},
size: 40,
enableResizing: false,
},
{
id: 'id',
header: 'ID',
accessorKey: 'id',
size: 80,
cell: ({ getValue }) => (
<span className="font-mono text-primary-400 text-xs block truncate">
{getValue() as string}
</span>
),
disableTooltip: true,
},
{
id: 'code',
header: 'Code',
accessorKey: 'code',
size: 100,
cell: ({ getValue }) => (
<span className="font-mono text-text-primary text-sm font-medium block truncate">
{getValue() as string}
</span>
),
size: 120,
cell: ({ getValue, row, cell }) => {
const isEditing =
editingCell?.id === row.original.id && editingCell?.field === 'code';
if (isEditing) {
return (
<input
type="text"
value={editValue}
onChange={e => setEditValue(e.target.value)}
onBlur={() => handleCellEdit(row.original.id, 'code', editValue)}
onKeyDown={e => {
if (e.key === 'Enter') {
handleCellEdit(row.original.id, 'code', editValue);
} else if (e.key === 'Escape') {
setEditingCell(null);
setEditValue('');
}
}}
className="w-full bg-surface border border-border rounded px-2 py-1 text-sm font-mono"
autoFocus
/>
);
}
return (
<div
className="cursor-pointer hover:bg-surface-secondary rounded px-2 py-1 transition-colors text-sm font-mono font-medium truncate overflow-hidden text-ellipsis whitespace-nowrap"
onClick={() => {
setEditingCell({ id: row.original.id, field: 'code' });
setEditValue(getValue() as string);
}}
>
{getValue() as string}
</div>
);
},
},
{
id: 'name',
@ -185,6 +217,7 @@ export function ExchangesTable() {
header: 'Active',
accessorKey: 'active',
size: 80,
disableTooltip: true,
cell: ({ getValue, row }) => {
const isActive = getValue() as boolean;
return (
@ -205,31 +238,58 @@ export function ExchangesTable() {
header: 'Provider Mappings',
accessorKey: 'provider_mapping_count',
size: 180,
disableTooltip: true,
cell: ({ getValue, row }) => {
const totalMappings = parseInt(getValue() as string) || 0;
const activeMappings = parseInt(row.original.active_mapping_count) || 0;
const verifiedMappings = parseInt(row.original.verified_mapping_count) || 0;
const providers = row.original.providers;
// Parse provider mappings from the expanded data or from a summary field
const exchangeId = row.original.id;
const mappings = expandedRowData[exchangeId] || [];
const handleLoadMappings = async () => {
if (mappings.length === 0 && totalMappings > 0) {
const details = await fetchExchangeDetails(exchangeId);
if (details) {
setExpandedRowData(prev => ({
...prev,
[exchangeId]: details.provider_mappings
}));
}
}
};
return (
<div className="flex flex-col gap-1">
<div
className="flex flex-col gap-1 cursor-pointer"
onMouseEnter={handleLoadMappings}
>
<div className="text-sm">
<span className="text-text-primary font-medium">{totalMappings}</span>
<span className="text-text-muted"> total</span>
{activeMappings > 0 && (
<span className="ml-2 text-green-400 text-xs">
{activeMappings} active
</span>
)}
</div>
<div className="flex gap-2 text-xs">
<span className="text-green-400">
<CheckIcon className="h-3 w-3 inline mr-1" />
{activeMappings} active
</span>
<span className="text-blue-400">
{verifiedMappings} verified
</span>
</div>
{providers && (
<div className="text-xs text-text-muted truncate" title={providers}>
{providers}
{mappings.length > 0 ? (
<div className="flex flex-wrap gap-1 text-xs">
{mappings.slice(0, 3).map((mapping, index) => (
<span key={index} className={`${mapping.active ? 'text-text-primary' : 'text-text-muted'}`}>
<span className="font-bold text-primary-400">{mapping.provider.toLowerCase()}</span>
<span className="text-text-secondary">({mapping.provider_exchange_code})</span>
</span>
))}
{mappings.length > 3 && (
<span className="text-text-muted">+{mappings.length - 3} more</span>
)}
</div>
) : totalMappings > 0 ? (
<div className="text-xs text-text-muted">Hover to load mappings...</div>
) : (
<div className="text-xs text-text-muted">No mappings</div>
)}
</div>
);
@ -238,16 +298,27 @@ export function ExchangesTable() {
{
id: 'actions',
header: 'Actions',
size: 120,
size: 140,
disableTooltip: true,
cell: ({ row }) => (
<button
onClick={() => handleAddProviderMapping(row.original.id, row.original.name)}
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"
title="Add Provider Mapping"
>
<PlusIcon className="h-3 w-3" />
Add Mapping
</button>
<div className="flex gap-1">
<button
onClick={() => handleAddProviderMapping(row.original.id, row.original.name)}
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"
title="Add Provider Mapping"
>
<PlusIcon className="h-3 w-3" />
Add
</button>
<button
onClick={() => handleDeleteExchange(row.original.id, row.original.name)}
className="inline-flex items-center gap-1 px-2 py-1 bg-danger/20 text-danger rounded text-xs hover:bg-danger/30 transition-colors"
title="Delete Exchange"
>
<TrashIcon className="h-3 w-3" />
Delete
</button>
</div>
),
},
{
@ -265,10 +336,13 @@ export function ExchangesTable() {
}, [
editingCell,
editValue,
expandedRowData,
handleCellEdit,
handleToggleActive,
handleAddProviderMapping,
handleDeleteExchange,
handleRowExpand,
fetchExchangeDetails,
]);
if (error) {