Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 83 additions & 54 deletions src/components/data/Sv1ClientTable.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ChevronUp, ChevronDown, ChevronsUpDown } from 'lucide-react';
import {
Table,
TableBody,
Expand All @@ -10,16 +11,28 @@ import { formatHashrate, truncateHex } from '@/lib/utils';
import { cn } from '@/lib/utils';
import type { Sv1ClientInfo } from '@/types/api';

export type SortKey = 'client_id' | 'authorized_worker_name' | 'user_identity' | 'hashrate';

interface Sv1ClientTableProps {
clients: Sv1ClientInfo[];
isLoading?: boolean;
sortKey: SortKey;
sortDir: 'asc' | 'desc';
onSort: (key: SortKey) => void;
}

function SortIcon({ column, sortKey, sortDir }: { column: SortKey; sortKey: SortKey; sortDir: 'asc' | 'desc' }) {
if (column !== sortKey) return <ChevronsUpDown className="h-3.5 w-3.5 text-muted-foreground/50" />;
return sortDir === 'asc'
? <ChevronUp className="h-3.5 w-3.5" />
: <ChevronDown className="h-3.5 w-3.5" />;
}

/**
* Table component for displaying SV1 clients connected to Translator.
* SV1 clients are legacy mining hardware using Stratum V1 protocol.
*/
export function Sv1ClientTable({ clients, isLoading }: Sv1ClientTableProps) {
export function Sv1ClientTable({ clients, isLoading, sortKey, sortDir, onSort }: Sv1ClientTableProps) {
if (isLoading) {
return (
<div className="rounded-xl border border-border/40 bg-card/40 backdrop-blur-sm overflow-hidden shadow-sm">
Expand All @@ -37,60 +50,76 @@ export function Sv1ClientTable({ clients, isLoading }: Sv1ClientTableProps) {
No SV1 clients connected
</div>
) : (
<Table>
<TableHeader className="bg-muted/30">
<TableRow className="hover:bg-transparent border-border/40">
<TableHead className="w-[80px]">ID</TableHead>
<TableHead>Worker Name</TableHead>
<TableHead>User Identity</TableHead>
<TableHead>Hashrate</TableHead>
<TableHead className="hidden md:table-cell">Channel</TableHead>
<TableHead className="hidden lg:table-cell">Extranonce1</TableHead>
<TableHead className="hidden xl:table-cell">Version Rolling</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{clients.map((client) => (
<TableRow key={client.client_id} className="hover:bg-muted/20 border-border/40 group">
<TableCell className="font-mono text-xs text-muted-foreground">
{client.client_id}
</TableCell>
<TableCell className="font-medium">
<div className="flex items-center space-x-2">
<div className={cn(
"h-2.5 w-2.5 rounded-full shadow-sm",
client.hashrate !== null ? "bg-green-500" : "bg-muted-foreground"
)} />
<span>{client.authorized_worker_name || '-'}</span>
</div>
</TableCell>
<TableCell className="text-muted-foreground">
{client.user_identity || '-'}
</TableCell>
<TableCell className="font-mono">
{client.hashrate !== null ? formatHashrate(client.hashrate) : '-'}
</TableCell>
<TableCell className="hidden md:table-cell font-mono text-xs text-muted-foreground">
{client.channel_id !== null ? client.channel_id : '-'}
</TableCell>
<TableCell className="hidden lg:table-cell font-mono text-xs text-muted-foreground">
{truncateHex(client.extranonce1_hex, 4)}
</TableCell>
<TableCell className="hidden xl:table-cell">
{client.version_rolling_mask ? (
<span className="inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold bg-green-500/10 text-green-500 border-green-500/20">
{truncateHex(client.version_rolling_mask, 4)}
</span>
) : (
<span className="inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold bg-muted text-muted-foreground border-border">
Disabled
</span>
)}
</TableCell>
<Table>
<TableHeader className="bg-muted/30">
<TableRow className="hover:bg-transparent border-border/40">
<TableHead className="w-[80px] cursor-pointer select-none" onClick={() => onSort('client_id')}>
<span className="flex items-center gap-1 hover:text-foreground transition-colors">
ID <SortIcon column="client_id" sortKey={sortKey} sortDir={sortDir} />
</span>
</TableHead>
<TableHead className="cursor-pointer select-none" onClick={() => onSort('authorized_worker_name')}>
<span className="flex items-center gap-1 hover:text-foreground transition-colors">
Worker Name <SortIcon column="authorized_worker_name" sortKey={sortKey} sortDir={sortDir} />
</span>
</TableHead>
<TableHead className="cursor-pointer select-none" onClick={() => onSort('user_identity')}>
<span className="flex items-center gap-1 hover:text-foreground transition-colors">
User Identity <SortIcon column="user_identity" sortKey={sortKey} sortDir={sortDir} />
</span>
</TableHead>
<TableHead className="cursor-pointer select-none" onClick={() => onSort('hashrate')}>
<span className="flex items-center gap-1 hover:text-foreground transition-colors">
Hashrate <SortIcon column="hashrate" sortKey={sortKey} sortDir={sortDir} />
</span>
</TableHead>
<TableHead className="hidden md:table-cell">Channel</TableHead>
<TableHead className="hidden lg:table-cell">Extranonce1</TableHead>
<TableHead className="hidden xl:table-cell">Version Rolling</TableHead>
</TableRow>
))}
</TableBody>
</Table>
</TableHeader>
<TableBody>
{clients.map((client) => (
<TableRow key={client.client_id} className="hover:bg-muted/20 border-border/40 group">
<TableCell className="font-mono text-xs text-muted-foreground">
{client.client_id}
</TableCell>
<TableCell className="font-medium">
<div className="flex items-center space-x-2">
<div className={cn(
"h-2.5 w-2.5 rounded-full shadow-sm",
client.hashrate !== null ? "bg-green-500" : "bg-muted-foreground"
)} />
<span>{client.authorized_worker_name || '-'}</span>
</div>
</TableCell>
<TableCell className="text-muted-foreground">
{client.user_identity || '-'}
</TableCell>
<TableCell className="font-mono">
{client.hashrate !== null ? formatHashrate(client.hashrate) : '-'}
</TableCell>
<TableCell className="hidden md:table-cell font-mono text-xs text-muted-foreground">
{client.channel_id !== null ? client.channel_id : '-'}
</TableCell>
<TableCell className="hidden lg:table-cell font-mono text-xs text-muted-foreground">
{truncateHex(client.extranonce1_hex, 4)}
</TableCell>
<TableCell className="hidden xl:table-cell">
{client.version_rolling_mask ? (
<span className="inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold bg-green-500/10 text-green-500 border-green-500/20">
{truncateHex(client.version_rolling_mask, 4)}
</span>
) : (
<span className="inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold bg-muted text-muted-foreground border-border">
Disabled
</span>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</div>
);
Expand Down
37 changes: 28 additions & 9 deletions src/pages/UnifiedDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { MinerConnectionInfo } from '@/components/setup/MinerConnectionInfo';
import { Shell } from '@/components/layout/Shell';
import { StatCard } from '@/components/data/StatCard';
import { HashrateChart } from '@/components/data/HashrateChart';
import { Sv1ClientTable } from '@/components/data/Sv1ClientTable';
import { Sv1ClientTable, type SortKey } from '@/components/data/Sv1ClientTable';
import { usePoolData, useSv1ClientsData, useTranslatorHealth, useJdcHealth } from '@/hooks/usePoolData';
import { useHashrateHistory } from '@/hooks/useHashrateHistory';
import { useSetupStatus } from '@/hooks/useSetupStatus';
Expand All @@ -27,6 +27,8 @@ import type { Sv1ClientInfo } from '@/types/api';
export function UnifiedDashboard() {
const [searchTerm, setSearchTerm] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const [sortKey, setSortKey] = useState<SortKey>('client_id');
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc');
const itemsPerPage = 15;

// Get configured template mode from setup status
Expand Down Expand Up @@ -154,15 +156,25 @@ export function UnifiedDashboard() {
? (clientChannels?.total_extended || 0) + (clientChannels?.total_standard || 0)
: activeCount;

// Filter clients by search
// Filter and sort clients
const filteredClients = useMemo(() => {
if (!searchTerm) return allClients;
const term = searchTerm.toLowerCase();
return allClients.filter((c: Sv1ClientInfo) =>
c.authorized_worker_name?.toLowerCase().includes(term) ||
c.user_identity?.toLowerCase().includes(term)
);
}, [allClients, searchTerm]);
let list = allClients;
if (searchTerm) {
const term = searchTerm.toLowerCase();
list = allClients.filter((c: Sv1ClientInfo) =>
c.authorized_worker_name?.toLowerCase().includes(term) ||
c.user_identity?.toLowerCase().includes(term)
);
}
const nullLast = sortDir === 'asc' ? Infinity : -Infinity;
return [...list].sort((a, b) => {
const av = a[sortKey] ?? nullLast;
const bv = b[sortKey] ?? nullLast;
if (av < bv) return sortDir === 'asc' ? -1 : 1;
if (av > bv) return sortDir === 'asc' ? 1 : -1;
return 0;
});
}, [allClients, searchTerm, sortKey, sortDir]);

// Pagination
const totalPages = Math.ceil(filteredClients.length / itemsPerPage);
Expand Down Expand Up @@ -328,6 +340,13 @@ export function UnifiedDashboard() {
<Sv1ClientTable
clients={paginatedClients}
isLoading={sv1Loading}
sortKey={sortKey}
sortDir={sortDir}
onSort={(key) => {
if (key === sortKey) setSortDir(d => d === 'asc' ? 'desc' : 'asc');
else { setSortKey(key); setSortDir('asc'); }
setCurrentPage(1);
}}
/>

{/* Pagination Footer */}
Expand Down
Loading