Skip to main content
Glama

Convex MCP server

Official
by get-convex
Backups.tsx14.8 kB
import { Button } from "@ui/Button"; import { Tooltip } from "@ui/Tooltip"; import { Spinner } from "@ui/Spinner"; import { TimestampDistance } from "@common/elements/TimestampDistance"; import { toast } from "@common/lib/utils"; import { Sheet } from "@ui/Sheet"; import { LocalDevCallout } from "@common/elements/LocalDevCallout"; import { Callout } from "@ui/Callout"; import { Checkbox } from "@ui/Checkbox"; import { TextInput } from "@ui/TextInput"; import { Popover } from "@ui/Popover"; import { useDisablePeriodicBackup, useGetPeriodicBackupConfig, useConfigurePeriodicBackup, } from "api/backups"; import { useCurrentProject } from "api/projects"; import { useId, useMemo, useState } from "react"; import { DeploymentResponse, Team, TeamEntitlementsResponse, } from "generatedApi"; import Link from "next/link"; import { useQuery } from "convex/react"; import udfs from "@common/udfs"; import { useHasProjectAdminPermissions } from "api/roles"; import { ChevronDownIcon } from "@radix-ui/react-icons"; import { Combobox } from "@ui/Combobox"; import { BackupList } from "./BackupList"; import { BackupRestoreStatus } from "./BackupRestoreStatus"; import { BackupNowButton } from "./BackupListItem"; export function Backups({ team, deployment, entitlements, }: { team: Team; deployment: DeploymentResponse; entitlements: TeamEntitlementsResponse; }) { const project = useCurrentProject(); const existingExport = useQuery(udfs.latestExport.default); const periodicBackupsEnabled = entitlements.periodicBackupsEnabled ?? false; const maxCloudBackups = entitlements.maxCloudBackups ?? 2; const hasAdminPermissions = useHasProjectAdminPermissions( deployment.projectId, ); const canPerformActions = deployment.deploymentType !== "prod" || hasAdminPermissions; return ( <div className="flex h-full flex-col gap-4"> <div className="mb-4 flex flex-wrap items-center justify-between gap-4"> <h3 className="min-w-fit">Backup & Restore</h3> <span className="text-sm"> Use this page to automatically or manually backup and restore your deployment data.{" "} <Link href="https://docs.convex.dev/database/backup-restore" className="text-content-link" > Learn more </Link> </span> </div> <div className="scrollbar flex grow flex-col gap-8 overflow-auto pt-1 pl-1 lg:flex-row lg:overflow-hidden"> <div className="flex shrink-0 flex-col lg:w-60"> {periodicBackupsEnabled ? ( <AutomaticBackupSelector deployment={deployment} canPerformActions={canPerformActions} /> ) : ( <Tooltip tip="Automatic backups are only available on the Pro plan." className="flex gap-1" > <label className="flex cursor-not-allowed items-center gap-2 text-sm"> <Checkbox disabled checked={false} onChange={() => {}} /> Backup automatically </label> <span className="rounded-sm bg-util-accent px-1.5 py-0.5 text-xs font-semibold tracking-wider text-white uppercase" title="Only available on the Pro plan" > Pro </span> </Tooltip> )} <hr className="my-6 w-full" /> <BackupNowButton deployment={deployment} team={team} maxCloudBackups={maxCloudBackups} canPerformActions={canPerformActions} /> <BackupProCallouts team={team} periodicBackupsEnabled={periodicBackupsEnabled} maxCloudBackups={maxCloudBackups} /> </div> <div className="flex flex-col gap-4 pb-8 lg:grow lg:pb-0"> {existingExport && existingExport._creationTime < new Date("2024-11-15").getTime() && existingExport.state === "completed" && Date.now() < Number(existingExport.expiration_ts / BigInt(1000000)) && ( <Callout> <div> Looking for your last Snapshot Export? You can now use Cloud Backups to backup and restore your deployment data. Download your last snapshot{" "} <Link href={`/t/${team.slug}/${project?.slug}/${deployment.name}/settings/snapshots`} className="text-content-link hover:underline" > here </Link> . </div> </Callout> )} <BackupRestoreStatus deployment={deployment} team={team} /> <Sheet padding={false} className="min-h-72"> <BackupList team={team} targetDeployment={deployment} canPerformActions={canPerformActions} maxCloudBackups={maxCloudBackups} /> </Sheet> </div> </div> </div> ); } function BackupProCallouts({ team, periodicBackupsEnabled, maxCloudBackups, }: { team: Team; periodicBackupsEnabled: boolean; maxCloudBackups: number; }) { return ( <> {!periodicBackupsEnabled && ( <LocalDevCallout className="mt-6 flex-col" tipText="Tip: Run this to enable automatic backups locally:" command={`cargo run --bin big-brain-tool -- --dev grant-entitlement --team-entitlement periodic_backups_enabled --team-id ${team?.id} --reason "local" true --for-real`} /> )} {maxCloudBackups <= 2 && ( <LocalDevCallout className="mt-6 flex-col" tipText="Tip: Run this to increase the backup limit locally:" command={`cargo run --bin big-brain-tool -- --dev grant-entitlement --team-entitlement max_cloud_backups --team-id ${team?.id} --reason "local" 50 --for-real`} /> )} </> ); } function AutomaticBackupSelector({ deployment, canPerformActions, }: { deployment: DeploymentResponse; canPerformActions: boolean; }) { const periodicBackup = useGetPeriodicBackupConfig(deployment.id); const configurePeriodicBackup = useConfigurePeriodicBackup(deployment.id); const disablePeriodicBackup = useDisablePeriodicBackup(deployment.id); const [isSubmitting, setIsSubmitting] = useState(false); return ( <Tooltip tip={ !canPerformActions ? "You do not have permission to change the backup settings in production." : undefined } > <div className="flex flex-col gap-2"> <label className="mb-1 flex items-center gap-2 text-sm"> <Checkbox checked={!!periodicBackup} disabled={ periodicBackup === undefined || isSubmitting || !canPerformActions } onChange={async () => { setIsSubmitting(true); try { if (periodicBackup === null) { // Enable automatic backups // We randomize the default cron spec to spread out the backups // of users that don’t specify a custom time const randomHour = Math.floor(Math.random() * 24); const randomMinute = Math.floor(Math.random() * 60); const defaultCronspec = `${randomMinute} ${randomHour} * * *`; await configurePeriodicBackup({ cronspec: defaultCronspec }); } else { // Disable automatic backups await disablePeriodicBackup(); } } finally { setIsSubmitting(false); } }} /> Backup automatically{" "} {isSubmitting && ( <div> <Spinner /> </div> )} </label> {periodicBackup && ( <> <BackupScheduleSelector cronspec={periodicBackup.cronspec} deployment={deployment} disabled={!canPerformActions} /> <div> <TimestampDistance prefix="Next backup " date={new Date(periodicBackup.nextRun)} /> <p className="text-xs text-content-secondary"> ({new Date(periodicBackup.nextRun).toLocaleString()}{" "} {localTimezoneName()}) </p> </div> </> )} </div> </Tooltip> ); } export function BackupScheduleSelector({ cronspec, deployment, disabled, }: { cronspec: string; deployment: DeploymentResponse; disabled: boolean; }) { const parts = cronspec.split(" "); const [minutesUtc, hoursUtc, , , dayOfWeekPart = "*"] = parts; const isWeekly = dayOfWeekPart !== "*"; const dayOfWeekNum = isWeekly ? Number(dayOfWeekPart) : null; const defaultDayOfWeek = useMemo( () => // We randomize the default day of week to spread out the backups // of users that don’t specify a custom time Math.floor(Math.random() * 7), [], ); const date = new Date(); date.setUTCHours(+hoursUtc, +minutesUtc); return ( <Popover button={ <Button variant="neutral" className="relative w-full pr-10 pl-3 font-normal" disabled={disabled} > <span className="flex flex-col truncate"> {isWeekly ? `${ [ "Sundays", "Mondays", "Tuesdays", "Wednesdays", "Thursdays", "Fridays", "Saturdays", ][dayOfWeekNum!] } at ${new Intl.DateTimeFormat(undefined, { hour: "2-digit", minute: "2-digit", }).format(date)}` : `Daily at ${new Intl.DateTimeFormat(undefined, { hour: "2-digit", minute: "2-digit", }).format(date)}`} </span> <span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"> <ChevronDownIcon className="h-5 w-5 text-content-tertiary" aria-hidden="true" /> </span> </Button> } openButtonClassName="*:bg-background-tertiary" > {({ close }) => ( <BackupScheduleSelectorInner defaultValue={date} defaultPeriodicity={isWeekly ? "weekly" : "daily"} defaultDayOfWeek={dayOfWeekNum ?? defaultDayOfWeek} onClose={close} deployment={deployment} /> )} </Popover> ); } export function BackupScheduleSelectorInner({ defaultValue, defaultPeriodicity, defaultDayOfWeek, onClose, deployment, }: { defaultValue: Date; defaultPeriodicity: "daily" | "weekly"; defaultDayOfWeek: number; onClose: () => void; deployment: DeploymentResponse; }) { const configurePeriodicBackup = useConfigurePeriodicBackup(deployment.id); const initialValue = `${defaultValue.getHours().toString().padStart(2, "0")}:${defaultValue.getMinutes().toString().padStart(2, "0")}`; const [value, setValue] = useState(initialValue); const id = useId(); const [isSubmitting, setIsSubmitting] = useState(false); const [periodicity, setPeriodicity] = useState(defaultPeriodicity); const [selectedDow, setSelectedDow] = useState(defaultDayOfWeek); return ( <form className="flex min-w-72 flex-col items-end gap-3" onSubmit={async (e) => { e.preventDefault(); const [newHoursLocal, newMinutesLocal] = value.split(":"); const nowLocal = new Date(); nowLocal.setHours(+newHoursLocal, +newMinutesLocal); setIsSubmitting(true); try { if (periodicity === "daily") { await configurePeriodicBackup({ cronspec: `${nowLocal.getUTCMinutes()} ${nowLocal.getUTCHours()} * * *`, }); } else { await configurePeriodicBackup({ cronspec: `${nowLocal.getUTCMinutes()} ${nowLocal.getUTCHours()} * * ${selectedDow}`, expirationDeltaSecs: 14 * 24 * 60 * 60, // 14 days }); } } finally { setIsSubmitting(false); } toast("success", "Your backup schedule was modified."); onClose(); }} > <div className="flex w-full flex-col gap-3"> <div className="flex items-center gap-2 text-sm"> <label className="flex items-center gap-1"> <input type="radio" value="daily" checked={periodicity === "daily"} onChange={() => setPeriodicity("daily")} /> Daily </label> <label className="flex items-center gap-1"> <input type="radio" value="weekly" checked={periodicity === "weekly"} onChange={() => setPeriodicity("weekly")} /> Weekly </label> </div> {periodicity === "weekly" && ( <Combobox label="Day of week" buttonClasses="w-full" optionsWidth="full" options={[ { value: 0, label: "Sunday" }, { value: 1, label: "Monday" }, { value: 2, label: "Tuesday" }, { value: 3, label: "Wednesday" }, { value: 4, label: "Thursday" }, { value: 5, label: "Friday" }, { value: 6, label: "Saturday" }, ]} selectedOption={selectedDow} setSelectedOption={(dow) => dow !== null && setSelectedDow(dow)} disableSearch /> )} <TextInput id={id} type="time" label={`Time (${localTimezoneName()})`} value={value} onChange={(e) => setValue(e.target.value)} required /> <div className="flex w-full justify-end"> <Button type="submit" disabled={ value === initialValue && periodicity === defaultPeriodicity && defaultDayOfWeek === selectedDow } loading={isSubmitting} > Change </Button> </div> </div> </form> ); } function localTimezoneName(): string { return new Intl.DateTimeFormat(undefined, { timeZoneName: "short", }) .formatToParts(new Date()) .find((part) => part.type === "timeZoneName")!.value; }

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