Skip to main content
Glama

Convex MCP server

Official
by get-convex
PageTimeline.tsx15.8 kB
import { useState } from "react"; import { ConvexSubscriptionId, IndexRangeBounds, PageArguments, PageResult, } from "../shared/types"; import { compareKeys, compareValues, maximalKey, minimalKey, } from "../shared/compare"; import { cursorForSyncObject } from "../server/resolvers"; import { Key } from "../shared/types"; import { Divider } from "./Divider"; import { ChevronRightIcon } from "@radix-ui/react-icons"; import { ChevronDownIcon } from "@radix-ui/react-icons"; import { SchemaDefinition } from "convex/server"; export function PageTimelineOld({ pages }: { pages: any[] }) { const [selectedPage, setSelectedPage] = useState<number | null>(null); const [showDocuments, setShowDocuments] = useState(false); const [expandedDocs, setExpandedDocs] = useState<Set<string>>(new Set()); const toggleDocument = (id: string) => { const newExpanded = new Set(expandedDocs); if (newExpanded.has(id)) { newExpanded.delete(id); } else { newExpanded.add(id); } setExpandedDocs(newExpanded); }; return ( <div className="mt-4 space-y-4"> <div className="relative h-16 flex items-center justify-center"> <div className="flex items-center gap-2 relative"> <div className="absolute left-4 right-4 top-1/2 h-px bg-gray-300 -z-10" /> <div className="text-gray-500">-∞</div> {pages.map((page, i) => { const isLoading = page.state.kind === "loading"; const numDocs = isLoading ? 1 : page.state.result.results.length; return ( <div key={i} className={`h-10 cursor-pointer ${ isLoading ? "border-2 border-dashed border-gray-300" : "border border-gray-400" } ${selectedPage === i ? "border-blue-500 border-2" : ""} rounded-lg flex items-center justify-center bg-white hover:border-blue-400 transition-colors`} style={{ width: `${Math.max(32, Math.min(numDocs * 8, 96))}px`, }} title={`${isLoading ? "Loading..." : `${numDocs} documents`}`} onClick={() => setSelectedPage(selectedPage === i ? null : i)} > {isLoading ? ( <div className="w-1.5 h-1.5 rounded-full bg-gray-400" /> ) : ( <div className="flex gap-0.5 p-1"> {Array(page.state.result.results.length) .fill(0) .map((_, i) => ( <div key={i} className="w-1 h-1 rounded-full bg-blue-500" /> ))} </div> )} </div> ); })} <div className="text-gray-500">+∞</div> </div> </div> {/* Page Details */} {selectedPage !== null && ( <div className="bg-gray-50 rounded-lg p-4 text-sm space-y-2"> {pages[selectedPage].state.kind === "loading" ? ( <div className="text-gray-500">Loading page...</div> ) : ( <> <div className="font-medium">Page Details:</div> <div className="space-y-1"> <div> <span className="text-gray-500">Lower Bound:</span>{" "} {pages[selectedPage].state.result.lowerBound.kind === "predecessor" ? "-∞" : JSON.stringify( pages[selectedPage].state.result.lowerBound.value, )} </div> <div> <span className="text-gray-500">Upper Bound:</span>{" "} {pages[selectedPage].state.result.upperBound.kind === "successor" ? "+∞" : JSON.stringify( pages[selectedPage].state.result.upperBound.value, )} </div> {/* Combined Documents section */} <div> <button onClick={() => setShowDocuments(!showDocuments)} className="flex items-center gap-1 text-gray-700 hover:text-gray-900" > <span className="transform transition-transform duration-200" style={{ display: "inline-block", transform: `rotate(${ showDocuments ? "90deg" : "0deg" })`, }} > ▶ </span> <span className="text-gray-500">Documents:</span>{" "} {pages[selectedPage].state.result.results.length} </button> {showDocuments && ( <div className="mt-2 space-y-1 pl-4"> {pages[selectedPage].state.result.results.map( (doc: any) => ( <div key={doc._id} className="space-y-1"> <button onClick={() => toggleDocument(doc._id)} className="flex items-center gap-1 text-gray-600 hover:text-gray-900" > <span className="transform transition-transform duration-200" style={{ display: "inline-block", transform: `rotate(${ expandedDocs.has(doc._id) ? "90deg" : "0deg" })`, }} > ▶ </span> {doc._id} </button> {expandedDocs.has(doc._id) && ( <pre className="pl-4 text-xs bg-white p-2 rounded border border-gray-200 overflow-auto"> {JSON.stringify(doc, null, 2)} </pre> )} </div> ), )} </div> )} </div> </div> </> )} </div> )} </div> ); } type BaseDocEvent = { kind: "doc"; key: Key; value: any; }; type DocEvent = BaseDocEvent & { exactEvents?: Array<"target" | "upperBound" | "lowerBound" | "inRange">; predecessorEvents?: Array<"target" | "upperBound" | "lowerBound">; successorEvents?: Array<"target" | "upperBound" | "lowerBound">; }; type KeyEventKind = | "target" | "upperBound" | "lowerBound" | "rangeLowerBound" | "rangeUpperBound"; type KeyEvent = { kind: "key"; key: Key; events: Array<KeyEventKind>; }; type Event = DocEvent | KeyEvent; function addEventToDoc( doc: DocEvent, category: "exact" | "predecessor" | "successor", kind: "target" | "upperBound" | "lowerBound" | "inRange", ) { if (category === "exact") { doc.exactEvents = doc.exactEvents || []; doc.exactEvents.push(kind); } else if (category === "predecessor") { doc.predecessorEvents = doc.predecessorEvents || []; doc.predecessorEvents.push(kind as any); } else { doc.successorEvents = doc.successorEvents || []; doc.successorEvents.push(kind as any); } } function addKeyEvent(events: Array<KeyEvent>, kind: KeyEventKind, key: Key) { const event = events.find((e) => compareKeys(e.key, key) === 0); if (event === undefined) { events.push({ kind: "key", key, events: [kind] }); } else { event.events.push(kind); } } export function PageTimeline({ orderedPages, rangeBounds, syncSchema, }: { orderedPages: Array<{ args: PageArguments; state: { kind: "loading" } | { kind: "loaded"; result: PageResult }; pageSubscriptionId: ConvexSubscriptionId; }>; rangeBounds?: IndexRangeBounds; syncSchema: SchemaDefinition<any, any>; }) { const [selectedPage, setSelectedPage] = useState<number | null>(null); const rangeLowerBound = rangeBounds !== undefined ? minimalKey(rangeBounds) : undefined; const rangeUpperBound = rangeBounds !== undefined ? maximalKey(rangeBounds) : undefined; return ( <div className="mt-4 space-y-4"> <div className="flex flex-col items-center gap-2"> {orderedPages.map((page, i) => { const pageState = page.state; const isLoading = pageState.kind === "loading"; if (isLoading) { return <div>Loading...</div>; } const target = page.args.target; const lowerBound = pageState.result.lowerBound; const upperBound = pageState.result.upperBound; const docs = pageState.result.results; const docsWithKeys: Array<DocEvent> = docs.map((doc) => ({ kind: "doc" as const, value: doc, key: cursorForSyncObject( syncSchema, page.args.syncTableName, page.args.index, doc, ), })); let foundTarget = false; let foundLowerBound = false; let foundUpperBound = false; for (const doc of docsWithKeys) { if ( rangeLowerBound !== undefined && rangeUpperBound !== undefined && compareKeys(doc.key, rangeLowerBound) >= 0 && compareKeys(doc.key, rangeUpperBound) <= 0 ) { addEventToDoc(doc, "exact", "inRange"); } if ( compareValues(doc.key.value as any, target.value as any) === 0 ) { addEventToDoc(doc, "exact", "target"); foundTarget = true; } if (compareKeys(doc.key, lowerBound) === 0) { addEventToDoc(doc, lowerBound.kind, "lowerBound"); foundLowerBound = true; } if (compareKeys(doc.key, upperBound) === 0) { addEventToDoc(doc, upperBound.kind, "upperBound"); foundUpperBound = true; } } const keyEvents: Array<KeyEvent> = []; if (!foundTarget) { addKeyEvent(keyEvents, "target", target); } if (!foundLowerBound) { addKeyEvent(keyEvents, "lowerBound", lowerBound); } if (!foundUpperBound) { addKeyEvent(keyEvents, "upperBound", upperBound); } if (rangeLowerBound !== undefined) { addKeyEvent(keyEvents, "rangeLowerBound", rangeLowerBound); } if (rangeUpperBound !== undefined) { addKeyEvent(keyEvents, "rangeUpperBound", rangeUpperBound); } const allEvents: Array<Event> = [...docsWithKeys, ...keyEvents]; allEvents.sort((a, b) => compareKeys(a.key, b.key)); return ( <div className="flex flex-col w-full" onMouseEnter={() => setSelectedPage(i)} onMouseLeave={() => setSelectedPage(null)} > {allEvents.map((event) => { if (event.kind === "doc") { return renderDocEvent(event, selectedPage === i); } const isSelected = selectedPage === i; const isRangeEvent = event.events.includes("rangeLowerBound") || event.events.includes("rangeUpperBound"); return ( <Divider key={JSON.stringify(event.key)} style={ isSelected && isRangeEvent ? "border-blue-500 border-dashed" : isSelected ? "border-gray-900" : "border-gray-300" } > <div className="flex flex-col items-end gap-1 text-xs"> <AbbreviatedIndexKey indexKey={event.key} /> {event.events.join(", ")} </div> </Divider> ); })} </div> ); })} </div> </div> ); } function renderDocEvent(event: DocEvent, isSelected: boolean) { const isInRange = event.exactEvents !== undefined && event.exactEvents.includes("inRange"); const exactEventsWithoutInRange = event.exactEvents !== undefined ? event.exactEvents.filter((e) => e !== "inRange") : []; const docTrigger = ( <div key={event.value._id} className="flex flex-row items-center justify-between w-full" > <div className="flex flex-row items-center gap-2"> {isInRange && <div className="w-1 h-1 rounded-full bg-blue-500" />} {event.value._id} </div> <div className="flex flex-col items-end gap-1"> <AbbreviatedIndexKey indexKey={event.key} /> {exactEventsWithoutInRange.length > 0 && ( <div className="flex flex-row items-center gap-1 text-xs"> {exactEventsWithoutInRange.join(", ")} </div> )} </div> </div> ); return ( <div className={ isSelected ? "flex flex-col w-full border my-1 rounded-lg p-2 border-gray-900" : "flex flex-col w-full border my-1 rounded-lg p-2 border-gray-200" } > {event.predecessorEvents !== undefined && ( <Divider style={isSelected ? "border-gray-900" : "border-gray-300"}> <div className="flex flex-col items-end gap-1 text-xs"> {event.predecessorEvents.join(", ")} <AbbreviatedIndexKey indexKey={event.key} /> </div> </Divider> )} <Collapsible trigger={docTrigger}> <div className="p-2"> <pre className="text-xs bg-white p-2 rounded border border-gray-200 overflow-auto"> {JSON.stringify(event.value, null, 2)} </pre> </div> </Collapsible> {event.successorEvents !== undefined && ( <Divider style={isSelected ? "border-gray-900" : "border-gray-300"}> <div className="flex flex-col items-end gap-1 text-xs"> {event.successorEvents.join(", ")} <AbbreviatedIndexKey indexKey={event.key} /> </div> </Divider> )} </div> ); } function Collapsible({ trigger, children, }: { trigger: React.ReactNode; children: React.ReactNode; }) { const [expanded, setExpanded] = useState(false); return ( <div> <div className="text-xs cursor-pointer hover:text-blue-500 flex flex-row items-center gap-1 w-full" onClick={() => setExpanded(!expanded)} > {expanded ? <ChevronDownIcon /> : <ChevronRightIcon />} {trigger} </div> {expanded && children} </div> ); } function AbbreviatedIndexKey({ indexKey }: { indexKey: Key }) { const [expanded, setExpanded] = useState(false); const truncatedKey = JSON.stringify(indexKey).slice(0, 20) + "..."; const fullKey = JSON.stringify(indexKey); return ( <span className="text-xs cursor-pointer hover:text-blue-500" onClick={() => setExpanded(!expanded)} > {expanded ? fullKey : truncatedKey} </span> ); }

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/get-convex/convex-backend'

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