Skip to main content
Glama

Convex MCP server

Official
by get-convex
UsageBarChart.tsx10.7 kB
import * as Sentry from "@sentry/nextjs"; import { DailyMetric, DailyPerTagMetrics } from "hooks/usageMetrics"; import { Bar, BarChart, Legend, Rectangle, ResponsiveContainer, Tooltip, XAxis, YAxis, } from "recharts"; import { useMemo } from "react"; import groupBy from "lodash/groupBy"; import sumBy from "lodash/sumBy"; import { useMeasure } from "react-use"; import { ChartTooltip } from "@common/elements/ChartTooltip"; import { toNumericUTC } from "@common/lib/format"; import { QuantityType, formatQuantity, formatQuantityCompact, } from "./lib/formatQuantity"; import { UsageNoDataError } from "./TeamUsageError"; // To avoid having a bar displayed too wide, we set a minimum amount of days for the chart’s x-axis span. const MIN_DAY_SPAN = 6; // When there is only a data point, we have to set the bar width manually to make it appear (https://github.com/recharts/recharts/issues/3640). // This value has been measured manually on a desktop screen size, but it should also look good in other contexts where there is only one bar. const SINGLE_BAR_WIDTH = 91; const MS_IN_DAY = 24 * 60 * 60 * 1000; export function UsageStackedBarChart({ rows, entity, categories, categoryRenames = {}, quantityType = "unit", showCategoryTotals = true, }: { rows: DailyPerTagMetrics[]; entity: string; categories: { [tag: string]: { name: string; color: string; }; }; // Merge multiple categories together (e.g. to count cached and uncached queries together) categoryRenames?: { [sourceDataName: string]: string }; quantityType?: QuantityType; showCategoryTotals?: boolean; }) { const chartData = useMemo(() => { const filledData = []; const dateSet = new Set(rows.map(({ ds }) => toNumericUTC(ds))); // Find the range of dates const minDate = Math.min(...Array.from(dateSet)); const maxDate = Math.max(...Array.from(dateSet)); // Fill in the missing dates for (let date = minDate; date <= maxDate; date += MS_IN_DAY) { const row = rows.find(({ ds }) => toNumericUTC(ds) === date); filledData.push({ dateNumeric: date, ...(row ? Object.fromEntries( Object.entries( groupBy(row.metrics, (metric) => { const tag = categoryRenames[metric.tag] ?? metric.tag; if (!(tag in categories)) { Sentry.captureMessage( `Unexpected call tag “${tag}”`, "error", ); } return tag; }), ).map(([key, keyMetrics]) => [ key, sumBy(keyMetrics, (metric) => metric.value), ]), ) : Object.fromEntries(Object.keys(categories).map((key) => [key, 0]))), }); } return filledData; }, [rows, categoryRenames, categories]); const totalByTag = useMemo( () => Object.fromEntries( Object.entries( groupBy( rows.flatMap((row) => row.metrics), (metric) => categoryRenames[metric.tag] ?? metric.tag, ), ).map(([tag, entries]) => [ tag, sumBy(entries, (entry) => entry.value), ]), ), [rows, categoryRenames], ); if (!rows.some(({ metrics }) => metrics.some(({ value }) => value > 0))) { return <UsageNoDataError entity={entity} />; } return ( <div className="h-56"> <DailyChart data={chartData} quantityType={quantityType} showCategoryInTooltip > {Object.entries(categories).map(([tag, { name, color }]) => ( <Bar key={tag} dataKey={tag} className={color} name={` ${name}`} barSize={chartData.length === 1 ? SINGLE_BAR_WIDTH : undefined} isAnimationActive={false} stackId="stack" shape={(props: any) => { // eslint-disable-next-line react/prop-types const { dateNumeric, name: categoryName } = props; if ( typeof dateNumeric !== "number" || typeof categoryName !== "string" ) { Sentry.captureMessage("Invalid props in stacked bar", "error"); return <Rectangle {...props} />; } return <Rectangle {...props} />; }} /> ))} <Legend content={() => ( <div className="flex flex-wrap gap-3 pl-[72px]"> {Object.entries(categories).map(([tag, { name, color }]) => Object.hasOwn(totalByTag, tag) ? ( <span key={tag} className="mr-3 flex items-center gap-2"> <svg className="w-4" viewBox="0 0 50 50" aria-hidden> <circle cx="25" cy="25" r="25" className={color} /> </svg> <span> <span>{name}</span> {showCategoryTotals && ( <>: {formatQuantity(totalByTag[tag], quantityType)}</> )} </span> </span> ) : null, )} </div> )} /> </DailyChart> </div> ); } export function UsageBarChart({ rows, entity, quantityType = "unit", }: { rows: DailyMetric[]; entity: string; quantityType?: QuantityType; }) { // Sort rows and convert date to numeric values const chartData = useMemo(() => { const filledData = []; const dateSet = new Set(rows.map(({ ds }) => toNumericUTC(ds))); // Find the range of dates const minDate = Math.min(...Array.from(dateSet)); const maxDate = Math.max(...Array.from(dateSet)); // Fill in the missing dates for (let date = minDate; date <= maxDate; date += MS_IN_DAY) { const row = rows.find(({ ds }) => toNumericUTC(ds) === date); filledData.push({ dateNumeric: date, value: row ? row.value : 0, }); } return filledData; }, [rows]); if (!rows.some(({ value }) => value > 0)) { return <UsageNoDataError entity={entity} />; } return ( <div className="h-48 animate-fadeInFromLoading"> <DailyChart data={chartData} quantityType={quantityType}> <Bar dataKey="value" isAnimationActive={false} className="fill-chart-line-1" name={quantityType === "unit" ? entity : "bytes"} barSize={chartData.length === 1 ? SINGLE_BAR_WIDTH : undefined} minPointSize={4} /> </DailyChart> </div> ); } function DailyChart({ data, showCategoryInTooltip = false, children, quantityType, }: React.PropsWithChildren<{ data: { dateNumeric: number }[]; categoryInTooltip?: boolean; showCategoryInTooltip?: boolean; quantityType: QuantityType; }>) { const { daysWithValues, minDate, daysCount } = useMemo(() => { const values = new Set(data.map(({ dateNumeric }) => dateNumeric)); const min = Math.min(...values); const max = Math.max(...values); return { daysWithValues: values, minDate: min, daysCount: Math.max(MIN_DAY_SPAN, (max - min) / MS_IN_DAY) + 1, }; }, [data]); const [containerRef, { width: containerWidth }] = useMeasure<HTMLDivElement>(); const ticks = useMemo(() => { if (containerWidth === 0) { return []; } const graphMargin = 90; const minBarWidth = 50; const barsWidth = containerWidth - graphMargin; const dayWidth = barsWidth / daysCount; const daysByTick = Math.ceil(minBarWidth / dayWidth); const ticksCount = Math.ceil(daysCount / daysByTick); return [...Array(ticksCount).keys()] .map((i) => minDate + i * daysByTick * MS_IN_DAY) .filter((day) => daysWithValues.has(day)); }, [containerWidth, daysCount, minDate, daysWithValues]); return ( <div ref={containerRef} className="h-full animate-fadeInFromLoading"> <ResponsiveContainer width="100%" height="100%"> <BarChart data={data} className="text-xs text-content-primary"> <XAxis scale="time" type="number" domain={[ minDate - MS_IN_DAY / 2, minDate + (daysCount - 1) * MS_IN_DAY + MS_IN_DAY / 2, ]} axisLine={false} tickSize={0} tick={{ fill: "currentColor", }} ticks={ticks} dataKey="dateNumeric" padding={{ left: 12 }} tickFormatter={(dateNumeric) => new Date(dateNumeric).toLocaleDateString("en-us", { month: "short", day: "numeric", timeZone: "UTC", }) } /> <YAxis axisLine={false} tickSize={0} tickFormatter={(value) => formatQuantityCompact(value, quantityType) } padding={{ top: 8, bottom: 8 }} tick={{ fill: "currentColor", }} style={{ fontVariantNumeric: "tabular-nums", }} width={60} /> <Tooltip isAnimationActive={false} cursor={{ fill: undefined, // Set in globals.css }} allowEscapeViewBox={{ y: true }} content={({ active, payload, label }) => ( <ChartTooltip active={active} payload={payload?.map((dataPoint) => { const prefix = showCategoryInTooltip ? `${dataPoint.name}: ` : ""; const value = dataPoint.value as number; const suffix = !showCategoryInTooltip && quantityType === "unit" ? ` ${dataPoint.name}` : ""; return { ...dataPoint, formattedValue: prefix + formatQuantity(value, quantityType) + suffix, }; })} label={new Date(label).toLocaleDateString("en-us", { year: "numeric", month: "long", day: "numeric", timeZone: "UTC", })} showLegend={showCategoryInTooltip} /> )} labelClassName="font-semibold" /> {children} </BarChart> </ResponsiveContainer> </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