Skip to main content
Glama
ResultsTabs.tsx5.55 kB
import { useMemo, useRef, useState, useEffect, useCallback } from 'react'; import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react'; import { cn } from '../../lib/utils'; import { ResultsTable } from './ResultsTable'; import type { ResultTab } from './types'; import XIcon from '../icons/XIcon'; interface ResultsTabsProps { tabs: ResultTab[]; activeTabId: string | null; onTabSelect: (id: string) => void; onTabClose: (id: string) => void; isLoading?: boolean; } function formatTimestamp(date: Date): string { return date.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit', }); } export function ResultsTabs({ tabs, activeTabId, onTabSelect, onTabClose, isLoading, }: ResultsTabsProps) { const tabsContainerRef = useRef<HTMLDivElement>(null); const [canScrollLeft, setCanScrollLeft] = useState(false); const [canScrollRight, setCanScrollRight] = useState(false); const activeTab = useMemo( () => tabs.find((tab) => tab.id === activeTabId), [tabs, activeTabId] ); const updateScrollButtons = useCallback(() => { const container = tabsContainerRef.current; if (!container) return; const { scrollLeft, scrollWidth, clientWidth } = container; setCanScrollLeft(scrollLeft > 0); setCanScrollRight(scrollLeft + clientWidth < scrollWidth - 1); }, []); useEffect(() => { updateScrollButtons(); window.addEventListener('resize', updateScrollButtons); return () => window.removeEventListener('resize', updateScrollButtons); }, [updateScrollButtons, tabs]); const scroll = (direction: 'left' | 'right') => { const container = tabsContainerRef.current; if (!container) return; const scrollAmount = 150; container.scrollBy({ left: direction === 'left' ? -scrollAmount : scrollAmount, behavior: 'smooth', }); }; // Loading state (no tabs yet) if (isLoading && tabs.length === 0) { return ( <div className="border border-border rounded-lg bg-card p-8 text-center"> <div className="text-muted-foreground">Running query...</div> </div> ); } // Empty state if (tabs.length === 0) { return ( <div className="border border-border rounded-lg bg-card p-8 text-center"> <p className="text-muted-foreground text-sm"> Run a query to see results </p> </div> ); } return ( <div className="space-y-2"> {/* Tab bar */} <div className="relative flex items-center border-b border-border"> {/* Left scroll button */} {canScrollLeft && ( <button type="button" onClick={() => scroll('left')} className="absolute left-0 z-10 flex items-center justify-center w-6 h-full bg-background hover:bg-muted cursor-pointer" aria-label="Scroll tabs left" > <ChevronLeftIcon className="w-4 h-4 text-muted-foreground" /> </button> )} {/* Tabs container */} <div ref={tabsContainerRef} onScroll={updateScrollButtons} className={cn( "flex items-center gap-1 overflow-hidden", canScrollLeft && "pl-6", canScrollRight && "pr-6" )} > {tabs.map((tab) => ( <button key={tab.id} type="button" onClick={() => onTabSelect(tab.id)} className={cn( 'group flex items-center gap-1.5 px-3 py-1.5 text-sm whitespace-nowrap', 'border-b-2 -mb-px transition-colors cursor-pointer', tab.id === activeTabId ? 'border-primary text-foreground' : 'border-transparent text-muted-foreground hover:text-foreground' )} > <span>{formatTimestamp(tab.timestamp)}</span> {tab.error && ( <span className="w-1.5 h-1.5 rounded-full bg-destructive" aria-label="Error" /> )} <span role="button" tabIndex={0} aria-label="Close tab" onClick={(e) => { e.stopPropagation(); onTabClose(tab.id); }} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.stopPropagation(); onTabClose(tab.id); } }} className="opacity-0 group-hover:opacity-100 hover:bg-muted rounded p-0.5 transition-opacity" > <XIcon className="w-3 h-3" /> </span> </button> ))} </div> {/* Right scroll button */} {canScrollRight && ( <button type="button" onClick={() => scroll('right')} className="absolute right-0 z-10 flex items-center justify-center w-6 h-full bg-background hover:bg-muted cursor-pointer" aria-label="Scroll tabs right" > <ChevronRightIcon className="w-4 h-4 text-muted-foreground" /> </button> )} </div> {/* Active tab content */} {activeTab && ( <ResultsTable result={activeTab.result} error={activeTab.error} isLoading={isLoading} executedSql={activeTab.executedSql} executionTimeMs={activeTab.executionTimeMs} /> )} </div> ); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/bytebase/dbhub'

If you have feedback or need assistance with the MCP directory API, please join our Discord server