Skip to main content
Glama

Convex MCP server

Official
by get-convex
BackupListItem.tsx24 kB
import { ArchiveIcon, ArrowDownIcon, ArrowRightIcon, CrossCircledIcon, DotsVerticalIcon, ExclamationTriangleIcon, GlobeIcon, MinusCircledIcon, Pencil2Icon, } from "@radix-ui/react-icons"; import { Button } from "@ui/Button"; import { Tooltip } from "@ui/Tooltip"; import { Loading } from "@ui/Loading"; import { Spinner } from "@ui/Spinner"; import { TimestampDistance } from "@common/elements/TimestampDistance"; import { toast } from "@common/lib/utils"; import { Callout } from "@ui/Callout"; import { Modal } from "@ui/Modal"; import { Checkbox } from "@ui/Checkbox"; import { Menu, MenuItem } from "@ui/Menu"; import { useEffect, useId, useRef, useState } from "react"; import { DeploymentResponse, ProjectDetails, Team } from "generatedApi"; import { useDeploymentById } from "api/deployments"; import { useTeamMembers } from "api/teams"; import { useProjects } from "api/projects"; import { useProfile } from "api/profile"; import { CommandLineIcon, ServerIcon, SignalIcon, } from "@heroicons/react/24/outline"; import { useRequestCloudBackup, useRestoreFromCloudBackup, useDeleteCloudBackup, BackupResponse, useListCloudBackups, useCancelCloudBackup, } from "api/backups"; import { Doc, Id } from "system-udfs/convex/_generated/dataModel"; import { BackupIdentifier } from "elements/BackupIdentifier"; import { cn } from "@ui/cn"; import { getDeploymentLabel } from "elements/DeploymentDisplay"; export function BackupListItem({ backup, restoring, someBackupInProgress, someRestoreInProgress, latestBackupInTargetDeployment, targetDeployment, team, canPerformActions, getZipExportUrl, maxCloudBackups, progressMessage, }: { backup: BackupResponse; restoring: boolean; someBackupInProgress: boolean; someRestoreInProgress: boolean; latestBackupInTargetDeployment: BackupResponse | null; targetDeployment: DeploymentResponse; team: Team; canPerformActions: boolean; getZipExportUrl: (snapshotId: Id<"_exports">) => string; maxCloudBackups: number; progressMessage: string | null; }) { const [modal, setModal] = useState< null | "suggestBackup" | "restoreConfirmation" | "delete" | "cancel" >(null); const previousState = useRef(backup.state); useEffect(() => { if ( previousState.current !== "complete" && backup.state === "complete" && backup.sourceDeploymentId === targetDeployment.id ) { toast("success", "Backup completed successfully."); } previousState.current = backup.state; }, [backup.state, backup.sourceDeploymentId, targetDeployment.id]); let backupStateDescription; if (backup.state === "requested" || backup.state === "inProgress") { backupStateDescription = "This backup hasn't completed yet."; } else if (backup.state === "complete") { backupStateDescription = "This backup is complete."; } else if (backup.state === "canceled") { backupStateDescription = "This backup was canceled."; } else if (backup.state === "failed") { backupStateDescription = "This backup failed."; } else { backup.state satisfies never; } return ( <> <div className="flex w-full flex-col"> <div className="my-2 flex flex-wrap items-center gap-4"> <div className="flex flex-1 flex-col items-start gap-1"> <span className="min-w-fit text-sm font-medium"> Backup from {new Date(backup.requestedTime).toLocaleString()}{" "} <span className="text-xs font-normal text-content-secondary"> (<TimestampDistance date={new Date(backup.requestedTime)} />) </span> </span> <BackupIdentifier backup={backup} /> {(backup.state === "requested" || backup.state === "inProgress") && ( <div className="flex items-center gap-2 text-content-secondary"> <Spinner /> {progressMessage || "In progress"} </div> )} </div> {backup.expirationTime !== null && ( <TimestampDistance prefix="Expires " date={new Date(backup.expirationTime)} className="text-left text-content-errorSecondary" /> )} {backup.state === "failed" && ( <Tooltip tip="This backup couldn’t be completed. Contact support@convex.dev for help." side="right" > <div className="flex items-center gap-1 text-content-errorSecondary"> <CrossCircledIcon /> <span className="border-b border-dotted border-b-content-errorSecondary/50"> Failed </span> </div> </Tooltip> )} {backup.state === "canceled" && ( <div className="flex items-center gap-1 text-content-secondary"> <MinusCircledIcon /> Canceled </div> )} <div> {restoring ? ( <div className="flex items-center gap-2 text-content-secondary"> <Spinner /> Restoring… </div> ) : ( <Menu buttonProps={{ size: "xs", variant: "neutral", icon: <DotsVerticalIcon />, title: "Backup options", }} placement="bottom-end" > {backup.state === "requested" || backup.state === "inProgress" ? ( <MenuItem variant="danger" action={() => setModal("cancel")} disabled={backup.state !== "inProgress"} tipSide="left" tip={ backup.state !== "inProgress" ? "This backup hasn't started yet." : !canPerformActions ? "You do not have permission to cancel backups in production." : undefined } > Cancel </MenuItem> ) : null} <MenuItem disabled={ backup.state !== "complete" || backup.sourceDeploymentId !== targetDeployment.id } href={ backup.state === "complete" ? getZipExportUrl(backup.snapshotId) : "" // only when disabled } tipSide="left" tip={ backup.state !== "complete" ? backupStateDescription : backup.sourceDeploymentId !== targetDeployment.id ? "You may download this backup from the Backup & Restore page for the deployment in which this backup was created." : null } > Download </MenuItem> <MenuItem action={() => { setModal( latestBackupInTargetDeployment === null || !isInLastFiveMinutes(latestBackupInTargetDeployment) ? "suggestBackup" : "restoreConfirmation", ); }} disabled={ backup.state !== "complete" || someRestoreInProgress || someBackupInProgress || !canPerformActions } tipSide="left" tip={ backup.state !== "complete" ? backupStateDescription : someRestoreInProgress ? "Another backup is being restored at the moment." : someBackupInProgress ? "Please wait for the ongoing backup to be completed before restoring from a backup." : !canPerformActions ? "You do not have permission to restore backups in production." : undefined } > Restore </MenuItem> <MenuItem variant="danger" action={() => setModal("delete")} disabled={ backup.state === "inProgress" || backup.state === "requested" || !canPerformActions } tipSide="left" tip={ backup.state === "inProgress" || backup.state === "requested" ? backupStateDescription : !canPerformActions ? "You do not have permission to delete backups in production." : undefined } > Delete </MenuItem> </Menu> )} </div> </div> </div> {(modal === "restoreConfirmation" || modal === "suggestBackup") && ( <Modal onClose={() => setModal(null)} title={ modal === "suggestBackup" ? "Backup before restoring?" : "Restore from a backup" } size="md" > {modal === "suggestBackup" ? ( <SuggestBackup team={team} targetDeployment={targetDeployment} onClose={() => setModal(null)} onContinue={() => setModal("restoreConfirmation")} latestBackupInTargetDeployment={latestBackupInTargetDeployment} maxCloudBackups={maxCloudBackups} canPerformActions={canPerformActions} /> ) : ( <RestoreConfirmation backup={backup} targetDeployment={targetDeployment} team={team} latestBackupInTargetDeployment={latestBackupInTargetDeployment} onClose={() => setModal(null)} /> )} </Modal> )} {(modal === "delete" || modal === "cancel") && ( <DeleteOrCancelBackupModal action={modal} backup={backup} team={team} onClose={() => setModal(null)} /> )} </> ); } function SuggestBackup({ team, targetDeployment, onClose, onContinue, latestBackupInTargetDeployment, maxCloudBackups, canPerformActions, }: { team: Team; targetDeployment: DeploymentResponse; onClose: () => void; onContinue: () => void; latestBackupInTargetDeployment: BackupResponse | null; maxCloudBackups: number; canPerformActions: boolean; }) { return ( <> <p> Restoring the backup will erase the data currently in the deployment. </p> <DeploymentSummary deployment={targetDeployment} latestBackup={latestBackupInTargetDeployment} team={team} /> <div className="flex justify-end gap-2"> <BackupNowButton deployment={targetDeployment} team={team} maxCloudBackups={maxCloudBackups} canPerformActions={canPerformActions} onBackupRequested={onClose} /> <Button variant="primary" onClick={onContinue}> Continue </Button> </div> </> ); } function RestoreConfirmation({ backup, targetDeployment, team, latestBackupInTargetDeployment, onClose, }: { backup: BackupResponse; targetDeployment: DeploymentResponse; team: Team; latestBackupInTargetDeployment: BackupResponse | null; onClose: () => void; }) { const requestRestore = useRestoreFromCloudBackup(targetDeployment.id); const [isSubmitting, setIsSubmitting] = useState(false); const needsCheckboxConfirmation = targetDeployment.deploymentType === "prod"; const [checkboxConfirmation, setCheckboxConfirmation] = useState(false); const checkboxConfirmationId = useId(); return ( <> <TransferSummary backup={backup} targetDeployment={targetDeployment} latestBackupInTargetDeployment={latestBackupInTargetDeployment} team={team} /> <p className="my-2"> The data (tables and files) in <code>{targetDeployment.name}</code> will be replaced by the contents of the backup. </p> <p className="text-content-secondary"> The rest of your deployment configuration (code, environment variables, scheduled functions, etc.) will not be changed. </p> {needsCheckboxConfirmation && ( <Callout className="mt-4 w-fit"> <label className="block" htmlFor={checkboxConfirmationId}> <Checkbox id={checkboxConfirmationId} className="mr-2" checked={checkboxConfirmation} onChange={() => setCheckboxConfirmation(!checkboxConfirmation)} /> I understand that this will <strong>erase</strong> my current{" "} <strong>production data</strong>. </label> </Callout> )} <div className="mt-4 flex justify-end"> <Button variant="neutral" onClick={async () => { setIsSubmitting(true); try { await requestRestore({ id: backup.id, }); } finally { setIsSubmitting(false); } onClose(); }} disabled={needsCheckboxConfirmation && !checkboxConfirmation} loading={isSubmitting} > Restore </Button> </div> </> ); } function DeleteOrCancelBackupModal({ action, backup, team, onClose, }: { action: "delete" | "cancel"; backup: BackupResponse; team: Team; onClose: () => void; }) { const doDelete = useDeleteCloudBackup(team.id, backup.id); const doCancel = useCancelCloudBackup(team.id, backup.id); const [isSubmitting, setIsSubmitting] = useState(false); return ( <Modal onClose={onClose} title={action === "delete" ? "Delete backup" : "Cancel backup"} > <p className="text-content-secondary">This action cannot be undone.</p> <BackupSummary backup={backup} sourceDeploymentAppearance="inline" team={team} /> <div className="flex justify-end gap-2"> <Button variant="danger" onClick={async () => { setIsSubmitting(true); try { if (action === "delete") { await doDelete(); } else if (action === "cancel") { await doCancel(); } else { action satisfies never; } } finally { setIsSubmitting(false); } onClose(); }} loading={isSubmitting} > {action === "delete" ? "Delete" : "Cancel"} Backup </Button> </div> </Modal> ); } export type BackupSummaryProps = { backup: BackupResponse | null; sourceDeploymentAppearance: null | "inline" | "differentDeploymentWarning"; team: Team; }; function BackupSummary({ backup, sourceDeploymentAppearance, team, }: BackupSummaryProps) { const backupDeployment = useDeploymentById( team.id, backup?.sourceDeploymentId, ); const sourceDeployment = backupDeployment ? ( <Tooltip tip={<code>{backupDeployment.name}</code>}> <FullDeploymentName deployment={backupDeployment} team={team} /> </Tooltip> ) : ( <div className="h-16 min-w-52"> <Loading /> </div> ); return ( <div className="my-8 flex flex-col items-center gap-2"> <p className="flex items-center gap-2 text-content-tertiary"> <ArchiveIcon className="size-6" /> Backup </p> <div className="flex flex-col items-center gap-1 text-center"> {backup !== null ? ( <> <p> Backup from <br /> {new Date(backup.requestedTime).toLocaleString()} </p> <p className="text-xs text-content-secondary"> (<TimestampDistance date={new Date(backup.requestedTime)} />) </p> </> ) : ( <em>Unknown backup</em> )} </div> {sourceDeploymentAppearance === "inline" && sourceDeployment} {sourceDeploymentAppearance === "differentDeploymentWarning" && ( <div className="relative mt-4 rounded-md border border-util-warning px-6 py-3"> <p className="absolute top-0 left-0 w-full -translate-y-1/2 text-center text-xs text-yellow-700 dark:text-util-warning"> <span className="inline-flex items-center justify-center gap-1 bg-background-secondary px-2 py-1"> <ExclamationTriangleIcon className="size-4" /> From a different deployment </span> </p> <div className="mt-2 flex flex-col place-items-center items-center gap-1 text-xs"> {sourceDeployment} </div> </div> )} </div> ); } type DeploymentSummaryProps = { deployment: DeploymentResponse; // null = show no backup warning, undefined = show nothing latestBackup: BackupResponse | null | undefined; team: Team; }; function DeploymentSummary({ deployment, latestBackup, team, }: DeploymentSummaryProps) { return ( <div className="my-8 flex flex-col items-center gap-2"> <p className="flex items-center gap-2 text-content-tertiary"> <ServerIcon className="size-6" /> Deployment </p> <Tooltip tip={<code>{deployment.name}</code>}> <FullDeploymentName deployment={deployment} team={team} /> </Tooltip> {latestBackup !== undefined && <LatestBackup backup={latestBackup} />} </div> ); } export function TransferSummary({ backup, targetDeployment, latestBackupInTargetDeployment, team, }: { backup: BackupResponse | null; targetDeployment: DeploymentResponse; latestBackupInTargetDeployment: BackupResponse | null | undefined; team: Team; }) { return ( <div className="grid justify-center gap-2 md:flex md:gap-5"> <BackupSummary backup={backup} sourceDeploymentAppearance={ backup && backup.sourceDeploymentId !== targetDeployment.id ? "differentDeploymentWarning" : null } team={team} /> <div className="md:my-8"> <ArrowDownIcon className="size-6 text-content-tertiary md:hidden" /> <ArrowRightIcon className="hidden size-6 text-content-tertiary md:block" /> </div> <DeploymentSummary deployment={targetDeployment} team={team} latestBackup={latestBackupInTargetDeployment} /> </div> ); } export function FullDeploymentName({ deployment, team, showProjectName = true, }: { deployment: DeploymentResponse; team: Team; showProjectName?: boolean; }) { const projects = useProjects(team.id); const project = projects?.find((p) => p.id === deployment.projectId); if (projects !== undefined && !project) { throw new Error("Unknown project"); } const whoseName = useMemberName(project, deployment); return ( <div className="flex flex-wrap items-center gap-2"> {showProjectName && ( <> {project === undefined ? ( <span className="inline-block h-6 w-32"> <Loading /> </span> ) : ( <span>{project.name}</span> )} <span className="text-content-secondary">/</span> </> )} <DeploymentLabel deployment={deployment} whoseName={whoseName ?? null} /> </div> ); } function useMemberName( project: ProjectDetails | undefined, deployment: DeploymentResponse | undefined, ) { const teamMembers = useTeamMembers(project?.teamId); const whose = teamMembers?.find((tm) => tm.id === deployment?.creator); const profile = useProfile(); const whoseName = whose?.email === profile?.email ? profile?.name || profile?.email : whose?.name || whose?.email || "Teammate"; return whoseName; } function LatestBackup({ backup }: { backup: BackupResponse | null }) { return ( <p> {backup === null ? ( <span className="text-xs text-content-errorSecondary">No backup</span> ) : ( <TimestampDistance prefix="Last backup created" className="text-inherit" date={new Date(backup.requestedTime)} /> )} </p> ); } function isInLastFiveMinutes(backup: BackupResponse): boolean { const fiveMinutes = 5 * 60 * 1000; return backup.requestedTime >= Date.now() - fiveMinutes; } export function BackupNowButton({ deployment, team, maxCloudBackups, canPerformActions, onBackupRequested, }: { deployment: DeploymentResponse; team: Team; maxCloudBackups: number; canPerformActions: boolean; onBackupRequested?: () => void; }) { const backups = useListCloudBackups(team?.id); const nonFailedBackupsForDeployment = backups?.filter( (backup) => backup.sourceDeploymentName === deployment.name && (backup.state === "requested" || backup.state === "inProgress" || backup.state === "complete"), ); const requestBackup = useRequestCloudBackup(deployment.id, team.id); const [isOngoing, setIsOngoing] = useState(false); const doBackup = async () => { setIsOngoing(true); try { await requestBackup(); } finally { setIsOngoing(false); } }; return ( <Button variant="neutral" className="w-fit" loading={isOngoing} icon={<ArchiveIcon />} onClick={async () => { await doBackup(); if (onBackupRequested) { onBackupRequested(); } }} disabled={ nonFailedBackupsForDeployment === undefined || nonFailedBackupsForDeployment.length >= maxCloudBackups || !canPerformActions } tip={ isOngoing ? "A backup is currently in progress." : nonFailedBackupsForDeployment && nonFailedBackupsForDeployment.length >= maxCloudBackups ? `You can only have up to ${maxCloudBackups} backups on your current plan. Delete some of your existing backups in this deployment to create a new one.` : !canPerformActions ? "You do not have permission to create backups in production." : undefined } > Backup Now </Button> ); } export function progressMessageForBackup( // Backup response from big-brain backup: BackupResponse, // Existing cloud backup within the backend existingCloudBackup: Doc<"_exports"> | null | undefined, ) { return existingCloudBackup?.state === "in_progress" && backup.state === "inProgress" && existingCloudBackup._id === backup.snapshotId ? existingCloudBackup.progress_message || null : null; } function DeploymentLabel({ whoseName, deployment, }: { deployment: DeploymentResponse; whoseName: string | null; }) { return ( <div className={cn("flex items-center gap-2 rounded-md")}> {deployment.deploymentType === "dev" ? ( deployment.kind === "local" ? ( <CommandLineIcon className="size-4" /> ) : ( <GlobeIcon className="size-4" /> ) ) : deployment.deploymentType === "prod" ? ( <SignalIcon className="size-4" /> ) : deployment.deploymentType === "preview" ? ( <Pencil2Icon className="size-4" /> ) : null} {getDeploymentLabel({ deployment, whoseName, })} </div> ); }

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