Skip to main content
Glama

Convex MCP server

Official
by get-convex
Logs.tsx8.43 kB
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState, } from "react"; import { useDebounce, usePrevious } from "react-use"; import isEqual from "lodash/isEqual"; import { dismissToast, toast } from "@common/lib/utils"; import { LogList } from "@common/features/logs/components/LogList"; import { LogToolbar } from "@common/features/logs/components/LogToolbar"; import { filterLogs } from "@common/features/logs/lib/filterLogs"; import { NENT_APP_PLACEHOLDER, Nent } from "@common/lib/useNents"; import { itemIdentifier, useModuleFunctions, } from "@common/lib/functions/FunctionsProvider"; import { functionIdentifierValue } from "@common/lib/functions/generateFileTree"; import { MAX_LOGS, UdfLog, useLogs } from "@common/lib/useLogs"; import { useDeploymentAuditLogs } from "@common/lib/useDeploymentAuditLog"; import { TextInput } from "@ui/TextInput"; import { Button } from "@ui/Button"; import { useGlobalLocalStorage } from "@common/lib/useGlobalLocalStorage"; import { DeploymentInfoContext } from "@common/lib/deploymentContext"; import { MultiSelectValue } from "@ui/MultiSelectCombobox"; export function Logs({ nents: allNents, selectedNent, }: { nents: Nent[]; selectedNent: Nent | null; }) { const { useCurrentDeployment } = useContext(DeploymentInfoContext); const deployment = useCurrentDeployment(); const deploymentPrefix = deployment?.name; const nents = allNents.filter((nent) => nent.name !== null); const logsConnectivityCallbacks = useRef({ onReconnected: () => { dismissToast("logStreamError"); toast("info", "Reconnected to log stream.", "logStreamReconnected"); }, onDisconnected: () => { dismissToast("logStreamReconnected"); toast( "error", "Disconnected from log stream. Will attempt to reconnect automatically.", "logStreamError", false, ); }, }); // Manage state for filter text. const [filter, setFilter] = useGlobalLocalStorage( `logs/${deploymentPrefix}/filter`, "", ); // Manage state for current log levels. const [levels, setLevels] = useGlobalLocalStorage<MultiSelectValue>( `logs/${deploymentPrefix}/levels`, "all", ); const defaultSelectedNent: MultiSelectValue = "all"; const [selectedNents, setSelectedNents] = useGlobalLocalStorage<MultiSelectValue>( `logs/${deploymentPrefix}/selectedNents`, defaultSelectedNent, ); // When the selected nent changes from props, update the storage if not already set useEffect(() => { if ( selectedNent && selectedNents !== "all" && !selectedNents.includes(selectedNent.path) ) { setSelectedNents([selectedNent.path]); } }, [selectedNent, selectedNents, setSelectedNents]); const moduleFunctions = useModuleFunctions(); const functions = useMemo( () => [ ...moduleFunctions.map((value) => itemIdentifier(value)), functionIdentifierValue("_other"), ], [moduleFunctions], ); const defaultSelectedFunctions: MultiSelectValue = "all"; const [selectedFunctions, setSelectedFunctions] = useGlobalLocalStorage<MultiSelectValue>( `logs/${deploymentPrefix}/selectedFunctions`, defaultSelectedFunctions, ); const [logs, setLogs] = useState<UdfLog[]>([]); const [filteredLogs, setFilteredLogs] = useState<UdfLog[]>([]); const filters = useMemo( () => ({ logTypes: levels, functions, selectedNents, selectedFunctions, filter, }), [filter, functions, levels, selectedFunctions, selectedNents], ); const previousFilters = usePrevious(filters); const [clearedLogs, setClearedLogs] = useState<number[]>([]); const [fromTimestamp, setFromTimestamp] = useState<number>(); const deploymentAuditLogs = useDeploymentAuditLogs(fromTimestamp); const receiveLogs = useCallback( (entries: UdfLog[]) => { setLogs((prev) => [...prev, ...entries].slice( Math.max(prev.length + entries.length - MAX_LOGS, 0), prev.length + entries.length, ), ); setFilteredLogs((prev) => [...prev, ...(filterLogs(filters, entries) || [])].slice( Math.max(prev.length + entries.length - MAX_LOGS, 0), prev.length + entries.length, ), ); }, [filters], ); const [manuallyPaused, setManuallyPaused] = useState(false); const [paused, setPaused] = useState<number>(0); const onPause = (p: boolean) => { const now = new Date().getTime(); setPaused(p ? now : 0); }; useLogs( logsConnectivityCallbacks.current, receiveLogs, paused > 0 || manuallyPaused, ); useEffect(() => { if (isEqual(filters, previousFilters)) { return; } const newFilteredLogs = filterLogs(filters, logs) || []; setFilteredLogs(newFilteredLogs); }, [filters, previousFilters, logs]); const [innerFilter, setInnerFilter] = useState(filter); useDebounce( () => { setFilter(innerFilter); }, 200, [innerFilter], ); // Function to set filter that also updates the text input const setFilterAndInput = useCallback( (newFilter: string) => { setFilter(newFilter); setInnerFilter(newFilter); }, [setFilter], ); // Note: fromTimestamp used to be a `useMemo` result, but it was causing a bug // where fromTimestamp would keep changing and causing the query to be refetched // every time the first log entry changed // (which shouldn't happen, but I haven't debugged why that does happen yet). useEffect(() => { if (logs && logs[0] && fromTimestamp === undefined) { setFromTimestamp(logs[0].timestamp); } }, [logs, fromTimestamp]); const latestLog = logs?.at(-1); const latestAuditLog = deploymentAuditLogs?.at(-1); const latestTimestamp = (latestLog?.timestamp ?? 0) > (latestAuditLog?._creationTime ?? 0) ? latestLog?.timestamp : latestAuditLog?._creationTime; return ( <div className="flex h-full w-full min-w-[48rem] flex-col overflow-hidden p-6 py-4"> <div className="flex shrink-0 flex-col gap-4"> <LogToolbar firstItem={<LogsHeader />} selectedLevels={levels} selectedFunctions={selectedFunctions} setSelectedFunctions={setSelectedFunctions} functions={functions} setSelectedLevels={setLevels} nents={ nents.length >= 1 ? [NENT_APP_PLACEHOLDER, ...nents.map((nent) => nent.path)] : undefined } selectedNents={selectedNents} setSelectedNents={setSelectedNents} /> <div className="mb-2 flex w-full gap-2"> <TextInput id="Search logs" placeholder="Filter logs..." value={innerFilter} onChange={(e) => setInnerFilter(e.target.value)} type="search" /> <Button size="sm" variant="neutral" tip="Clear the currently visible logs to declutter this page." tipSide="left" disabled={ latestTimestamp === undefined || !logs || (clearedLogs.length ? logs.filter( (log) => log.timestamp > clearedLogs[clearedLogs.length - 1], ) : logs ).length === 0 } onClick={() => { setClearedLogs([...clearedLogs, latestTimestamp!]); }} > Clear Logs </Button> </div> </div> <div className="flex-1 overflow-hidden"> <LogList nents={nents} logs={logs} filteredLogs={filteredLogs} deploymentAuditLogs={deploymentAuditLogs} filter={filter} setFilter={setFilterAndInput} clearedLogs={clearedLogs} setClearedLogs={setClearedLogs} paused={paused > 0 || manuallyPaused} setPaused={onPause} setManuallyPaused={(p) => { onPause(p); setManuallyPaused(p); }} /> </div> </div> ); } function LogsHeader() { return ( <div className="mr-2 flex grow items-center justify-between gap-2"> <div className="flex items-center gap-2"> <h3>Logs</h3> </div> </div> ); }

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