UNPKG

@mcp-shark/mcp-shark

Version:

Aggregate multiple Model Context Protocol (MCP) servers into a single unified interface with a powerful monitoring UI. Prov deep visibility into every request and response.

184 lines (170 loc) 5.92 kB
import { useState, useEffect, useMemo, useRef } from 'react'; import { colors, fonts } from './theme'; import RequestRow from './components/RequestRow'; import TableHeader from './components/TableHeader'; import ViewModeTabs from './components/ViewModeTabs'; import GroupedByMcpView from './components/GroupedByMcpView'; import { groupByMcpSessionAndCategory } from './utils/mcpGroupingUtils.js'; import { pairRequestsWithResponses } from './utils/requestUtils.js'; import { staggerIn } from './utils/animations'; import anime from 'animejs'; function RequestList({ requests, selected, onSelect, firstRequestTime }) { const [viewMode, setViewMode] = useState('general'); const [columnWidths] = useState({ frame: 90, // Frame numbers like "#1234" time: 120, // Relative time like "0.123456" datetime: 180, // Full date/time like "11/30/2024, 19:25:30" source: 200, // IP addresses or hostnames destination: 200, // IP addresses or hostnames protocol: 90, // Usually "HTTP" method: 90, // HTTP methods like "POST", "GET" status: 80, // Status codes like "200", "404" endpoint: 500, // JSON-RPC methods or URLs (most important, needs space) }); const [expandedResponses, setExpandedResponses] = useState(new Set()); const [expandedMcpSessions, setExpandedMcpSessions] = useState(new Set()); const [expandedMcpCategories, setExpandedMcpCategories] = useState(new Set()); const tbodyRef = useRef(null); const prevRequestsLengthRef = useRef(0); const groupedByMcp = useMemo(() => groupByMcpSessionAndCategory(requests), [requests]); // Animate rows when requests change useEffect(() => { if (tbodyRef.current && requests.length > 0) { const rows = tbodyRef.current.querySelectorAll('tr'); if (rows.length > 0) { // Only animate new rows if the list has grown if (requests.length > prevRequestsLengthRef.current) { const newRows = Array.from(rows).slice(prevRequestsLengthRef.current); if (newRows.length > 0) { staggerIn(newRows, { delay: 30, duration: 300 }); } } else { // Animate all rows if the list was reset staggerIn(rows, { delay: 20, duration: 300 }); } prevRequestsLengthRef.current = requests.length; } } }, [requests]); useEffect(() => { if (viewMode === 'groupedByMcp') { // Auto-expand all MCP sessions and categories const allSessionIds = new Set(groupedByMcp.map((g) => g.sessionId || '__NO_SESSION__')); setExpandedMcpSessions((prev) => { const updated = new Set(prev); allSessionIds.forEach((id) => updated.add(id)); return updated; }); setExpandedMcpCategories((prev) => { const updated = new Set(prev); groupedByMcp.forEach((sessionGroup) => { const sessionKey = sessionGroup.sessionId || '__NO_SESSION__'; sessionGroup.categories.forEach((category) => { updated.add(`${sessionKey}::${category.category}`); }); }); return updated; }); } }, [groupedByMcp, viewMode]); const toggleMcpSession = (sessionKey) => { setExpandedMcpSessions((prev) => { const updated = new Set(prev); if (updated.has(sessionKey)) { updated.delete(sessionKey); } else { updated.add(sessionKey); } return updated; }); }; const toggleMcpCategory = (categoryKey) => { setExpandedMcpCategories((prev) => { const updated = new Set(prev); if (updated.has(categoryKey)) { updated.delete(categoryKey); } else { updated.add(categoryKey); } return updated; }); }; const pairedRequests = useMemo(() => pairRequestsWithResponses(requests), [requests]); const toggleResponse = (frameNumber) => { setExpandedResponses((prev) => { const updated = new Set(prev); if (updated.has(frameNumber)) { updated.delete(frameNumber); } else { updated.add(frameNumber); } return updated; }); }; const renderGeneralView = () => ( <tbody ref={tbodyRef}> {pairedRequests.map((pair) => ( <RequestRow key={pair.frame_number} pair={pair} selected={selected} firstRequestTime={firstRequestTime} onSelect={onSelect} isExpanded={expandedResponses.has(pair.frame_number)} onToggleExpand={() => toggleResponse(pair.frame_number)} /> ))} </tbody> ); return ( <div style={{ flex: 1, display: 'flex', flexDirection: 'column', background: colors.bgPrimary, minHeight: 0, overflow: 'hidden', }} > <ViewModeTabs viewMode={viewMode} onViewModeChange={setViewMode} /> <div style={{ flex: 1, overflowY: 'auto', overflowX: 'auto', minHeight: 0, WebkitOverflowScrolling: 'touch', }} > <table style={{ width: '100%', borderCollapse: 'separate', borderSpacing: 0, fontSize: '12px', fontFamily: fonts.body, background: colors.bgPrimary, }} > <TableHeader columnWidths={columnWidths} /> {viewMode === 'general' ? ( renderGeneralView() ) : ( <GroupedByMcpView groupedData={groupedByMcp} expandedSessions={expandedMcpSessions} expandedCategories={expandedMcpCategories} onToggleSession={toggleMcpSession} onToggleCategory={toggleMcpCategory} selected={selected} firstRequestTime={firstRequestTime} onSelect={onSelect} /> )} </table> </div> </div> ); } export default RequestList;