Skip to main content
Glama

Convex MCP server

Official
by get-convex
SpendingLimits.tsx10.6 kB
import * as Yup from "yup"; import React, { useCallback, useEffect, useId } from "react"; import { TextInput } from "@ui/TextInput"; import { Form, Formik, getIn, useFormikContext } from "formik"; import { Button } from "@ui/Button"; import { useSetSpendingLimit } from "api/billing"; import { Loading } from "@ui/Loading"; import { Checkbox } from "@ui/Checkbox"; import { cn } from "@ui/cn"; import { ExclamationTriangleIcon, QuestionMarkCircledIcon, } from "@radix-ui/react-icons"; import { Tooltip } from "@ui/Tooltip"; import Link from "next/link"; import { formatUsd } from "@common/lib/utils"; import { Team } from "generatedApi"; import * as Sentry from "@sentry/nextjs"; export type SpendingLimitsValue = { // null = disabled (= checkbox unchecked) // "" = enabled but no value set spendingLimitWarningThresholdUsd: null | "" | number; spendingLimitDisableThresholdUsd: null | "" | number; }; export function spendingLimitsSchema({ currentSpending, }: { currentSpending: | { totalCents: number; nextBillingPeriodStart: string } | undefined; }) { const baseSchema = Yup.mixed() .test( "is-spending-value", "Please enter a positive number.", (value) => value === null || (typeof value === "number" && value >= 0), ) .test( "is-integer-or-null", "Please enter an integer amount.", (value) => value === null || Number.isInteger(value), ); const disableSchema = baseSchema.test( "is-greater-than-current-spending", currentSpending !== undefined ? `The spend limit must be greater than the spending in the current billing cycle (${formatUsd(currentSpending.totalCents / 100)}). ${ currentSpending.nextBillingPeriodStart ? `You will be able to lower your spending limit at the start of the next billing cycle (${formatDate( currentSpending.nextBillingPeriodStart, )} at midnight UTC).` : "" }` : "", (value) => currentSpending === undefined || value === null || currentSpending.totalCents <= value * 100, ); return Yup.object().shape({ spendingLimitDisableThresholdUsd: disableSchema, spendingLimitWarningThresholdUsd: baseSchema.test( "is-less-than-spend-limit", "The warning threshold must be less than the spend limit.", function isLessThanSpendLimitValidator(warningThreshold) { const { parent } = this; const disableThreshold = parent.spendingLimitDisableThresholdUsd; // If the disable threshold is 0, this field should always be valid // (it will be hidden in the UI and set to null) if (disableThreshold === 0) { return true; } return ( typeof warningThreshold !== "number" || typeof disableThreshold !== "number" || warningThreshold < disableThreshold ); }, ), }); } export function useSubmitSpendingLimits(team: Team) { const setSpendingLimit = useSetSpendingLimit(team.id); return useCallback( async (v: SpendingLimitsValue) => { await setSpendingLimit(spendingLimitValueToCents(v)); }, [setSpendingLimit], ); } export function spendingLimitValueToCents(value: SpendingLimitsValue) { let { spendingLimitWarningThresholdUsd, spendingLimitDisableThresholdUsd } = value; if (spendingLimitWarningThresholdUsd === "") { spendingLimitWarningThresholdUsd = null; Sentry.captureMessage( "Spending limits form submitted with empty warning threshold", "error", ); } if (spendingLimitDisableThresholdUsd === "") { spendingLimitDisableThresholdUsd = null; Sentry.captureMessage( "Spending limits form submitted with empty disable threshold", "error", ); } return { warningThresholdCents: typeof spendingLimitWarningThresholdUsd === "number" ? spendingLimitWarningThresholdUsd * 100 : spendingLimitWarningThresholdUsd, disableThresholdCents: typeof spendingLimitDisableThresholdUsd === "number" ? spendingLimitDisableThresholdUsd * 100 : spendingLimitDisableThresholdUsd, }; } export function SpendingLimitsForm({ defaultValue, onSubmit, onCancel, currentSpending, }: { defaultValue: SpendingLimitsValue | undefined; onSubmit: (values: SpendingLimitsValue) => Promise<void>; onCancel: () => void; currentSpending: | { totalCents: number; nextBillingPeriodStart: string; } | undefined; }) { const isLoading = defaultValue === undefined; return ( <Formik enableReinitialize initialValues={ isLoading ? { spendingLimitWarningThresholdUsd: null, spendingLimitDisableThresholdUsd: null, } : defaultValue } validationSchema={spendingLimitsSchema({ currentSpending })} onSubmit={async (values) => { await onSubmit(values); }} > {({ isSubmitting }) => ( <Form className="flex flex-col items-start gap-4"> {isLoading ? ( <Loading className="h-[176px] w-full max-w-64" fullHeight={false} /> ) : ( <SpendingLimits /> )} <div className="flex gap-2"> <Button type="submit" disabled={isLoading || isSubmitting}> {isSubmitting ? "Saving Spending Limits…" : "Save Spending Limits"} </Button> <Button variant="neutral" onClick={onCancel}> Cancel </Button> </div> </Form> )} </Formik> ); } /** To use within a Formik form with a state that is a superset of `SpendingLimitsValue` */ export function SpendingLimits() { const { values, setFieldValue, touched } = useFormikContext<SpendingLimitsValue>(); const { spendingLimitWarningThresholdUsd, spendingLimitDisableThresholdUsd } = values; useEffect(() => { if (spendingLimitDisableThresholdUsd === 0) { void setFieldValue("spendingLimitWarningThresholdUsd", null); } }, [ spendingLimitDisableThresholdUsd, setFieldValue, touched.spendingLimitDisableThresholdUsd, ]); return ( <div className="flex w-full flex-col gap-4"> {spendingLimitDisableThresholdUsd !== 0 && ( <SpendLimitInput formKey="spendingLimitWarningThresholdUsd" label="Warn when spending exceeds" accessibleInputLabel="Warning Threshold" disabled={spendingLimitDisableThresholdUsd === 0} description={ spendingLimitWarningThresholdUsd !== null && ( <> If your <UsageDefinition /> exceeds this amount, <br /> admins in your team will be notified by email. </> ) } /> )} <SpendLimitInput formKey="spendingLimitDisableThresholdUsd" label="Limit usage spending to" accessibleInputLabel="Disable Threshold" description={ spendingLimitDisableThresholdUsd !== null && ( <span className="mt-0.5 flex gap-1.5 text-content-warning"> <ExclamationTriangleIcon /> <span className="block flex-1"> If your <UsageDefinition /> exceeds{" "} {spendingLimitDisableThresholdUsd === 0 ? "the built-in limits of your plan, " : "this amount, "} <strong className="font-semibold"> all of your team’s projects will be disabled </strong>{" "} until you increase the spend limit. </span> </span> ) } /> </div> ); } function UsageDefinition() { return ( <Tooltip tip={ <> Resources used beyond the{" "} <Link className="text-content-link hover:underline" href="https://www.convex.dev/pricing" > built-in resources of your plan </Link> . Seat fees are not counted in your spending limits. </> } side="right" > <div className="flex gap-0.5"> <span className="underline decoration-dotted">usage</span> <QuestionMarkCircledIcon /> </div> </Tooltip> ); } function SpendLimitInput({ formKey, label, accessibleInputLabel, description, disabled = false, }: { formKey: | "spendingLimitWarningThresholdUsd" | "spendingLimitDisableThresholdUsd"; label: string; accessibleInputLabel: string; description?: React.ReactNode; disabled?: boolean; }) { const formState = useFormikContext<SpendingLimitsValue>(); const error = formState.errors[formKey]; const value = formState.values[formKey]; const checkboxId = useId(); const inputId = useId(); const inputDisabled = value === null || disabled; return ( <div className="flex max-w-64 flex-col gap-1"> <label className="flex items-center gap-2 text-sm text-content-primary" htmlFor={checkboxId} > <Checkbox id={checkboxId} checked={value !== null} onChange={() => { void formState.setFieldValue(formKey, value === null ? "" : null); }} disabled={disabled} /> {label} </label> <TextInput id={inputId} type="number" {...formState.getFieldProps(formKey)} value={value ?? ""} label={accessibleInputLabel} labelHidden description={description} min={0} step={1} error={getIn(formState.touched, formKey) && error} leftAddon={ <div className={cn( "w-4 text-center text-sm", inputDisabled ? "text-content-tertiary" : "text-content-primary", )} > $ </div> } rightAddon={ <div className="text-sm text-content-secondary">/ month</div> } className="pr-16" disabled={inputDisabled} /> </div> ); } function formatDate(dateString: string) { const parts = dateString.split("-"); if (parts.length !== 3) { throw new Error("Invalid date string"); } const year = parseInt(parts[0], 10); const month = parseInt(parts[1], 10) - 1; const day = parseInt(parts[2], 10); return new Date(year, month, day).toLocaleDateString(undefined, { year: "numeric", month: "long", day: "numeric", }); }

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