Skip to main content
Glama

Convex MCP server

Official
by get-convex
DataContent.tsx15.4 kB
import { useRouter } from "next/router"; import { useCallback, useContext, useEffect, useMemo, useRef, useState, } from "react"; import { FixedSizeList } from "react-window"; import { useQuery } from "convex/react"; import udfs from "@common/udfs"; import classNames from "classnames"; import { Filter, FilterExpression, SchemaJson, } from "system-udfs/convex/_system/frontend/lib/filters"; import { Shape } from "shapes"; import { LoadingLogo, LoadingTransition } from "@ui/Loading"; import { Sheet } from "@ui/Sheet"; import { Button } from "@ui/Button"; import { DeploymentInfoContext } from "@common/lib/deploymentContext"; import { useSelectionState } from "@common/features/data/lib/useSelectionState"; import { useDataToolbarActions } from "@common/features/data/lib/useDataToolbarActions"; import { useTableFilters } from "@common/features/data/lib/useTableFilters"; import { useToolPopup } from "@common/features/data/lib/useToolPopup"; import { useAuthorizeProdEdits } from "@common/features/data/lib/useAuthorizeProdEdits"; import { usePatchDocumentField } from "@common/features/data/components/Table/utils/usePatchDocumentField"; import { Table, TableSkeleton, } from "@common/features/data/components/Table/Table"; import { DataToolbar, DataToolbarSkeleton, } from "@common/features/data/components/DataToolbar/DataToolbar"; import { EmptyDataContent } from "@common/features/data/components/EmptyData"; import { useDataColumns } from "@common/features/data/components/Table/utils/useDataColumns"; import { useQueryFilteredTable } from "@common/features/data/components/Table/utils/useQueryFilteredTable"; import { useSingleTableSchemaStatus } from "@common/features/data/components/TableSchema"; import { DataFilters } from "@common/features/data/components/DataFilters/DataFilters"; import { useTableFields } from "@common/features/data/components/Table/utils/useTableFields"; import { useDefaultDocument } from "@common/features/data/lib/useDefaultDocument"; import { ImperativePanelHandle, Panel, PanelGroup, } from "react-resizable-panels"; import { cn } from "@ui/cn"; import { getDefaultIndex } from "@common/features/data/components/DataFilters/IndexFilters"; import { api } from "system-udfs/convex/_generated/api"; import { useNents } from "@common/lib/useNents"; import omit from "lodash/omit"; import { clearFilters } from "./DataFilters/clearFilters"; export function DataContent({ tableName, shape, componentId, activeSchema, }: { tableName: string; componentId: string | null; shape: Shape | null; activeSchema: SchemaJson | null; }) { const { filters, applyFiltersWithHistory, hasFilters } = useTableFilters( tableName, componentId, ); const [draftFilters, setDraftFilters] = useState(filters); const [showFilters, setShowFilters] = useState(false); useEffect(() => { setDraftFilters(filters); }, [filters]); const router = useRouter(); const tableSchemaStatus = useSingleTableSchemaStatus(tableName); const numRowsInTable = useQuery(udfs.tableSize.default, { tableName, componentId, }); const hasFiltersAndAtLeastOneDocument = hasFilters && numRowsInTable !== undefined && numRowsInTable > 0; const { status, loadNextPage, staleAsOf, isLoading, data, errors, numRowsReadEstimate, isPaused, } = useQueryFilteredTable(tableName); const numRowsRead = Math.min(numRowsReadEstimate, numRowsInTable || 0); const { useCurrentDeployment } = useContext(DeploymentInfoContext); const deployment = useCurrentDeployment(); const isProd = deployment?.deploymentType === "prod"; const localStorageKey = router.query && `${router.query.deploymentName}/${tableName}`; const ref = useRef<ImperativePanelHandle>(null); const allIds = useMemo( () => new Set(data.map((row) => row._id as string)), [data], ); const selectedRows = useSelectionState(allIds, status === "Exhausted"); const tableFields = useTableFields(tableName, shape, data); const columns = useDataColumns({ tableName, localStorageKey, fields: tableFields, data, // Subtract 3 border pixels, one on each side of the parent box // and one more on the right side of the last column. width: (ref.current?.getSize() || 1000) - 3, }); const listRef = useRef<FixedSizeList>(null); const scrollToTop = useCallback(() => listRef.current?.scrollToItem(0), []); const [rowsThatAreSelected, { reset: clearSelectedRows, all: allSelected }] = selectedRows; const [previousTableName, setPreviousTableName] = useState(tableName); if (tableName !== previousTableName) { setPreviousTableName(tableName); clearSelectedRows(); } const patchDocumentField = usePatchDocumentField(tableName); const [areEditsAuthorized, onAuthorizeEdits] = useAuthorizeProdEdits({ isProd, }); const { addDocuments, patchFields, clearTable, deleteTable, deleteRows } = useDataToolbarActions({ // Scrolling to the first item when a new document is added // for works now while we are guaranteed to be sorting by creation time. handleAddDocuments: scrollToTop, clearSelectedRows, loadMore: loadNextPage, tableName, }); const allRowsSelected = allSelected === true && !hasFiltersAndAtLeastOneDocument; const popupState = useToolPopup({ addDocuments: (table, docs) => addDocuments(table, docs), patchFields: (table, rowIds, fields) => patchFields(table, rowIds, fields), clearSelectedRows, clearTable, deleteRows: (rowIds) => deleteRows(rowIds), deleteTable, isProd, numRows: numRowsInTable, tableName, areEditsAuthorized, onAuthorizeEdits, activeSchema, }); const { popupEl } = popupState; // Handle query parameter to open the indexes panel useEffect(() => { if (!!router.query.showIndexes && !popupState.popup) { popupState.setPopup({ type: "viewIndexes", tableName }); void router.push( { pathname: router.pathname, query: omit(router.query, "showIndexes"), }, undefined, { shallow: true }, ); } }, [router.query.showIndexes, router, popupState, tableName]); const selectedDocumentId = rowsThatAreSelected.values().next().value; const selectedDocument = data.find((row) => row._id === selectedDocumentId); const defaultDocument = useDefaultDocument(tableName); const { selectedNent } = useNents(); const indexes = useQuery(api._system.frontend.indexes.default, { tableName, tableNamespace: selectedNent?.id ?? null, }) ?? undefined; const sortField = ( indexes?.find((index) => index.name === filters?.index?.name)?.fields as | string[] | undefined )?.[0] || "_creationTime"; const { captureMessage } = useContext(DeploymentInfoContext); useEffect(() => { if ( status !== "LoadingFirstPage" && !(data.length || status === "CanLoadMore") && !hasFiltersAndAtLeastOneDocument && !isLoading ) { captureMessage( `Encountered unexpected state in data page: status: ${status}, numRowsInTable: ${numRowsInTable}, numRowsRead: ${numRowsRead}, isLoading: ${isLoading}`, "warning", ); } }, [ status, data.length, hasFiltersAndAtLeastOneDocument, isLoading, numRowsInTable, numRowsRead, captureMessage, ]); return ( <PanelGroup direction="horizontal" className={cn( "scrollbar flex h-full w-full min-w-[20rem] overflow-x-auto pl-6", popupEl ? "pr-0" : "pr-6", )} autoSaveId="data-content" > <Panel className={cn( "flex shrink flex-col gap-2 overflow-hidden py-4", "max-w-full", popupEl ? "min-w-[16rem]" : "min-w-[20rem]", )} ref={ref} defaultSize={80} minSize={10} > <DataToolbar popupState={popupState} deleteRows={deleteRows} selectedRowsIds={rowsThatAreSelected} allRowsSelected={allRowsSelected === true} selectedDocument={selectedDocument} numRows={numRowsInTable} tableSchemaStatus={tableSchemaStatus} tableName={tableName} isProd={isProd} isLoadingMore={isLoading && !isPaused} /> <div className="flex h-full max-h-full flex-col overflow-y-hidden rounded-lg"> {numRowsInTable !== undefined && numRowsInTable > 0 && ( <DataFilters tableName={tableName} componentId={componentId} tableFields={tableFields} defaultDocument={defaultDocument} filters={filters} onFiltersChange={applyFiltersWithHistory} dataFetchErrors={errors} draftFilters={draftFilters} setDraftFilters={setDraftFilters} activeSchema={activeSchema} numRows={numRowsInTable} numRowsLoaded={data.length} hasFilters={hasFiltersAndAtLeastOneDocument} showFilters={showFilters} setShowFilters={setShowFilters} /> )} <LoadingTransition loadingState={ <div className="flex h-full flex-col items-center justify-center gap-8 rounded-lg border bg-background-secondary"> <LoadingLogo /> </div> } loadingProps={{ shimmer: false }} > {status !== "LoadingFirstPage" && (data.length || status === "CanLoadMore" ? ( <Sheet className={classNames("w-full relative rounded-t-none")} padding={false} > {!isPaused && staleAsOf > 0 && ( <LoadingFilteredData numRowsRead={numRowsRead} numRowsInTable={numRowsInTable} overlay /> )} <Table activeSchema={activeSchema} listRef={listRef} loadMore={loadNextPage} sort={{ order: filters?.order || "desc", field: sortField, }} totalRowCount={ router.query.filters ? status === "Exhausted" ? data.length : // If we are filtering, we need to add 1 to the total row count to // allow the infinite loader to load more documents when scrolling. data.length + 1 : numRowsInTable } hasFilters={hasFiltersAndAtLeastOneDocument} patchDocument={patchDocumentField} selectedRows={selectedRows} areEditsAuthorized={areEditsAuthorized} onAuthorizeEdits={onAuthorizeEdits} tableName={tableName} componentId={componentId} isProd={isProd} data={data} columns={columns} localStorageKey={localStorageKey} hasPopup={!!popupEl} setPopup={popupState.setPopup} deleteRows={deleteRows} defaultDocument={defaultDocument} onAddDraftFilter={(filter: Filter) => { setDraftFilters((prev) => prev ? { clauses: [...prev.clauses, filter], index: prev.index ?? getDefaultIndex(), } : { clauses: [filter], index: getDefaultIndex(), }, ); setShowFilters(true); }} /> </Sheet> ) : hasFiltersAndAtLeastOneDocument ? ( isLoading ? ( <Sheet className="flex w-full grow animate-fadeIn items-center justify-center rounded-t-none" padding={false} > <LoadingFilteredData numRowsRead={numRowsRead} numRowsInTable={numRowsInTable} /> </Sheet> ) : isEmptySearchFilter(filters) ? ( <div className="flex h-full flex-1 flex-col items-center gap-2 rounded-t-none border bg-background-secondary pt-8"> <div className="text-content-secondary"> Enter a search term to find matching documents. </div> <Button onClick={() => applyFiltersWithHistory(clearFilters(filters)) } size="xs" > Clear filters </Button> </div> ) : ( <div className="flex h-full flex-1 flex-col items-center gap-2 rounded-t-none border bg-background-secondary pt-8"> <div className="text-content-secondary"> No documents match the selected filters. </div> <Button onClick={() => applyFiltersWithHistory(clearFilters(filters)) } size="xs" > Clear filters </Button> </div> ) ) : isLoading || (numRowsInTable !== undefined && numRowsInTable > 0) ? null /* Loading */ : ( <EmptyDataContent openAddDocuments={() => popupState.setPopup({ type: "addDocuments", tableName }) } /> ))} </LoadingTransition> </div> </Panel> {popupEl} </PanelGroup> ); } function LoadingFilteredData({ numRowsRead, numRowsInTable, overlay = false, }: { numRowsRead: number; numRowsInTable: any; overlay?: boolean; }) { return ( <div className={classNames( "flex h-full w-full items-center justify-center", overlay && "absolute left-0 top-0 z-10 bg-white/75 dark:bg-black/75 animate-fadeIn", )} > <div className="flex animate-pulse flex-col items-center"> <p>Applying filters...</p> <p> Scanned {numRowsRead.toLocaleString()} of{" "} {numRowsInTable?.toLocaleString()} documents </p> </div> </div> ); } export function DataContentSkeleton() { return ( <div className="flex h-full grow flex-col gap-2 p-6"> <DataToolbarSkeleton /> <TableSkeleton /> </div> ); } function isEmptySearchFilter(filters: FilterExpression | undefined) { return ( filters?.index && "search" in filters.index && filters.index.search.trim() === "" ); }

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