Skip to main content
Glama

Convex MCP server

Official
by get-convex
UsageBanner.tsx6.97 kB
import { Cross2Icon, ExclamationTriangleIcon, InfoCircledIcon, } from "@radix-ui/react-icons"; import classNames from "classnames"; import { Button, buttonClasses } from "@ui/Button"; import { useUnpauseTeam } from "api/teams"; import { useTeamUsageState } from "api/usage"; import Link from "next/link"; import { useEffect, useState } from "react"; import { Team } from "generatedApi"; import { useGetSpendingLimits } from "api/billing"; export type Variant = | "Approaching" | "Exceeded" | "Disabled" | "Paused" | "ExceededSpendingLimit"; export function useCurrentUsageBanner(teamId: number | null): Variant | null { const { isDismissed } = useDismiss(teamId); const spendingLimits = useGetSpendingLimits(teamId); const currentVariantPro = spendingLimits.spendingLimits?.state === "Disabled" ? "ExceededSpendingLimit" : null; const currentVariantFree = useTeamUsageState(teamId); const currentVariant = currentVariantPro ?? currentVariantFree; if ( !currentVariant || currentVariant === "Default" || (isDismissable(currentVariant) && isDismissed) ) { return null; } return currentVariant; } export function UsageBanner({ variant, team, }: { variant: Variant; team: Team; }) { const { dismiss } = useDismiss(team.id); const { title, containerClass, primaryButtonClass, secondaryButtonClass, icon: Icon, } = getVariantDetails(variant); const primaryButtonClassFull = classNames( buttonClasses({ variant: variant === "Approaching" ? "primary" : "unstyled", size: "sm", }), primaryButtonClass, "px-2.5 py-2 rounded-sm text-sm font-medium", "ml-2", ); const secondaryButtonClassFull = classNames( buttonClasses({ variant: "unstyled", size: "sm", }), "hover:opacity-75", "px-1 py-2 rounded-sm text-xs font-medium", secondaryButtonClass, ); const unpauseTeam = useUnpauseTeam(team.id); const [isRestoringTeam, setIsRestoringTeam] = useState(false); return ( <div className={classNames( "grid grid-cols-[auto_1fr] sm:flex shrink-0 sm:h-12 h-24 items-center px-2 py-1 border-b gap-2 overflow-x-hidden", containerClass, )} > <Icon className="h-4 w-4" /> <div className="flex min-w-[12em] flex-1 items-center gap-1 text-xs"> {title} </div> <div className="col-span-2 flex items-center justify-end"> {variant === "Paused" ? ( <Button variant="unstyled" className={classNames( primaryButtonClassFull, "disabled:opacity-50 disabled:pointer-events-none", )} disabled={isRestoringTeam} onClick={async () => { setIsRestoringTeam(true); void unpauseTeam(); }} > Enable All Projects </Button> ) : variant === "ExceededSpendingLimit" ? ( <Link className={primaryButtonClassFull} href={`/${team.slug}/settings/billing`} > Billing Settings </Link> ) : ( <> <Link className={secondaryButtonClassFull} href={`/${team.slug}/settings/usage`} > View Usage </Link> <Link className={primaryButtonClassFull} href={`/${team.slug}/settings/billing`} > Upgrade </Link> </> )} {isDismissable(variant) && ( <Button className="ml-2 h-fit" variant="neutral" size="xs" inline title="Dismiss" onClick={dismiss} > <Cross2Icon /> </Button> )} </div> </div> ); } function isDismissable(variant: Variant) { return variant === "Approaching"; } function getVariantDetails(variant: Variant): { title: string; containerClass: string; primaryButtonClass: string; secondaryButtonClass: string; icon: React.FC<{ className: string | undefined }>; } { const dangerStyle = { // eslint-disable-next-line no-restricted-syntax containerClass: "bg-red-700 text-white", // eslint-disable-next-line no-restricted-syntax primaryButtonClass: "bg-red-100 text-red-900 hover:bg-red-300", secondaryButtonClass: "text-white", icon: ExclamationTriangleIcon, }; switch (variant) { case "Approaching": return { title: "Your projects are approaching the Free plan limits. Consider upgrading to avoid service interruption.", containerClass: "bg-blue-100 dark:bg-blue-900", primaryButtonClass: "", secondaryButtonClass: "text-blue-900 text-content-primary", icon: InfoCircledIcon, }; case "Exceeded": return { title: "Your projects are above the Free plan limits. Decrease your usage or upgrade to avoid service interruption.", containerClass: "bg-background-warning dark:text-white", primaryButtonClass: "bg-yellow-500 text-black hover:bg-yellow-700 hover:text-white", secondaryButtonClass: "text-cyan-900 dark:text-white", icon: ExclamationTriangleIcon, }; case "Disabled": return { title: "Your projects are disabled because the team exceeded Free plan limits. Decrease your usage or upgrade to re-enable your projects.", ...dangerStyle, }; case "Paused": return { title: // This is shown as disabled to the user to not confuse them "Your projects are disabled because the team previously exceeded Free plan limits.", ...dangerStyle, }; case "ExceededSpendingLimit": return { title: "Your projects are disabled because you exceeded your spending limit. Increase it to re-enable your projects.", ...dangerStyle, }; default: { variant satisfies never; throw new Error("Unexpected variant"); } } } function useDismiss(teamId: number | null) { const key = `usage-banner-dismissed-${teamId}`; const [isDismissed, setIsDismissed] = useState(true); useEffect(() => { if (teamId === null) { return undefined; } // Load the value from localStorage when the component mounts setIsDismissed(localStorage.getItem(key) !== null); // Get updates from other components that are also using useDismiss const listener = () => setIsDismissed(true); window.addEventListener(key, listener); return () => window.removeEventListener(key, listener); }, [teamId, key]); return { isDismissed, dismiss() { if (teamId !== null) { setIsDismissed(true); window.dispatchEvent(new Event(key)); localStorage.setItem(`usage-banner-dismissed-${teamId}`, "true"); } }, }; }

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