Skip to main content
Glama

Convex MCP server

Official
by get-convex
CommandPalette.tsx7.88 kB
import { Command } from "cmdk"; import { TrashIcon } from "@radix-ui/react-icons"; import { useCurrentProject, useDeleteProjects, useProjects, } from "api/projects"; import { useCurrentTeam } from "api/teams"; import React from "react"; import { Checkbox } from "@ui/Checkbox"; import { useHotkeys } from "react-hotkeys-hook"; import { buttonClasses } from "@ui/Button"; import { Spinner } from "@ui/Spinner"; import { TimestampDistance } from "@common/elements/TimestampDistance"; import { useLaunchDarkly } from "hooks/useLaunchDarkly"; import { Tooltip } from "@ui/Tooltip"; import { useClickAway } from "react-use"; import { cn } from "@ui/cn"; import { useRouter } from "next/router"; export function CommandPalette() { const [open, setOpen] = React.useState(false); const [search, setSearch] = React.useState(""); const [pages, setPages] = React.useState<string[]>([]); const page = pages[pages.length - 1]; useHotkeys(["meta+k", "ctrl+k"], (event) => { event.preventDefault(); setOpen((isOpen) => !isOpen); }); useHotkeys(["escape", "backspace"], (event) => { if ( pages.length > 0 && (event.key === "Escape" || (event.key === "Backspace" && !search)) ) { setPages((currentPages) => currentPages.slice(0, -1)); } else if (event.key === "Escape") { setOpen(false); } }); const ref = React.useRef<HTMLDivElement>(null); useClickAway(ref, () => { setOpen(false); }); const isTeamAdmin = true; const { commandPalette, commandPaletteDeleteProjects } = useLaunchDarkly(); if (!commandPalette) { return null; } return ( <Command.Dialog open={open} ref={ref} label="Convex Command Palette" title="Convex Command Palette" > <Command.Input placeholder={ page === "delete-projects" ? "Search projects..." : "What do you want to do?" } value={search} onValueChange={setSearch} /> <Command.List> {!page && ( <Command.Group heading="Projects"> {commandPaletteDeleteProjects && ( <Tooltip side="right" tip={ !isTeamAdmin ? "You must be a team admin to delete projects in bulk." : undefined } > <Command.Item onSelect={() => setPages((currentPages) => [ ...currentPages, "delete-projects", ]) } disabled={!isTeamAdmin} > <TrashIcon className="size-4" /> Delete Projects </Command.Item> </Tooltip> )} </Command.Group> )} {page === "delete-projects" && ( <DeleteProjectsPage onClose={() => setOpen(false)} /> )} <Command.Empty>No results found.</Command.Empty> </Command.List> </Command.Dialog> ); } function DeleteProjectsPage({ onClose }: { onClose: () => void }) { const router = useRouter(); const [projectIds, setProjectIds] = React.useState<number[]>([]); const [lastSelectedIndex, setLastSelectedIndex] = React.useState< number | null >(null); const currentTeam = useCurrentTeam(); const currentProject = useCurrentProject(); const projects = useProjects(currentTeam?.id); const deleteProjects = useDeleteProjects(currentTeam?.id); const [isSubmitting, setIsSubmitting] = React.useState(false); const handleDeleteProjects = async () => { if (projectIds.length === 0) { return; } setIsSubmitting(true); setProjectIds([]); try { if (currentProject && projectIds.includes(currentProject.id)) { await router.push(`/t/${currentTeam?.slug}`); } await deleteProjects({ projectIds }); onClose(); } finally { setTimeout(() => { setIsSubmitting(false); }, 0); } }; const toggleProject = ( projectId: number, index: number, event: React.MouseEvent, ) => { if (event.nativeEvent?.shiftKey && lastSelectedIndex !== null) { // Implement shift+click selection const start = Math.min(lastSelectedIndex, index); const end = Math.max(lastSelectedIndex, index); const isSelected = projectIds.includes(projectId); const newProjectIds = new Set(projectIds); if (isSelected) { // Unselect this row and all the next consecutive selected for (let i = start; i <= end; i++) { const id = projects?.[i]?.id; if (id && projectIds.includes(id)) { newProjectIds.delete(id); } } } else { // If there are no rows selected above, first try to select from below const firstSelected = projects?.findIndex((p) => projectIds.includes(p.id)) ?? -1; if (firstSelected > index) { for (let i = index; i < firstSelected; i++) { const id = projects?.[i]?.id; if (id) { newProjectIds.add(id); } } } else { // Select all rows from the first unselected row above for (let i = index; i >= 0; i--) { const id = projects?.[i]?.id; if (id && !projectIds.includes(id)) { newProjectIds.add(id); } else { break; } } } } setProjectIds(Array.from(newProjectIds)); } else { // Regular click behavior setProjectIds( projectIds.includes(projectId) ? projectIds.filter((id) => id !== projectId) : [...projectIds, projectId], ); } setLastSelectedIndex(index); }; return ( <Command.Group heading="Select projects to delete"> {isSubmitting && ( <Command.Loading> <div className="flex items-center gap-1 text-sm text-content-secondary"> <Spinner className="size-4" /> Submitting... </div> </Command.Loading> )} {!isSubmitting && projects?.map((project, index) => ( <Command.Item key={project.id} className="flex justify-between" keywords={[project.name, project.slug]} onSelect={(event) => toggleProject( project.id, index, event as unknown as React.MouseEvent, ) } > <div className="flex items-center gap-1"> <Checkbox className="mr-1" checked={projectIds.includes(project.id)} onChange={(event) => toggleProject( project.id, index, event as unknown as React.MouseEvent, ) } /> <p> {project.name}{" "} <span className="text-content-tertiary">({project.slug})</span> </p> </div> <TimestampDistance date={new Date(project.createTime)} prefix="Created" /> </Command.Item> ))} {projectIds.length > 0 && ( <Command.Item className={cn( buttonClasses({ size: "xs", variant: "neutral" }), "bottom-four absolute right-4 z-20 flex items-center gap-4 text-xs", )} data-button onSelect={handleDeleteProjects} forceMount > <TrashIcon className="size-4" /> Delete {projectIds.length}{" "} {projectIds.length === 1 ? "project" : "projects"} </Command.Item> )} </Command.Group> ); }

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