Fix(webapp): metrics UI improvements (#3063)

- Lots of small UI improvements
- Toggle full screen charts with "V" shortcut

<img width="3504" height="2286" alt="CleanShot 2026-02-14 at 20 06
30@2x"
src="https://github.com/user-attachments/assets/32403661-06b3-4f6c-a074-243b3f43197d"
/>

<img width="362" height="222" alt="CleanShot 2026-02-14 at 20 06 58@2x"
src="https://github.com/user-attachments/assets/ee4f5859-4b71-482e-9636-5abd78d9f623"
/>

<img width="434" height="360" alt="CleanShot 2026-02-14 at 20 06 53@2x"
src="https://github.com/user-attachments/assets/44339348-f3f3-425b-bbf3-bb41ed6a8e59"
/>

<img width="460" height="310" alt="CleanShot 2026-02-14 at 20 06 48@2x"
src="https://github.com/user-attachments/assets/eb3b6af6-b88a-4677-8a48-acfa6b768770"
/>

<img width="502" height="332" alt="CleanShot 2026-02-14 at 20 06 45@2x"
src="https://github.com/user-attachments/assets/b788d78b-8f4a-4b18-94c8-487673f3ec7b"
/>

<img width="370" height="257" alt="CleanShot 2026-02-14 at 20 10 29@2x"
src="https://github.com/user-attachments/assets/a81e8b14-b2eb-402c-bf3b-affd0d4fde26"
/>
This commit is contained in:
James Ritchie
2026-02-14 21:26:25 +00:00
committed by GitHub
parent 5d6085dcff
commit b4e08bddec
16 changed files with 201 additions and 215 deletions
+6
View File
@@ -193,6 +193,12 @@ function ShortcutContent() {
<ShortcutKey shortcut={{ key: "v" }} variant="medium/bright" />
</Shortcut>
</div>
<div className="space-y-3">
<Header3>Metrics page</Header3>
<Shortcut name="Toggle fullscreen chart">
<ShortcutKey shortcut={{ key: "v" }} variant="medium/bright" />
</Shortcut>
</div>
<div className="space-y-3">
<Header3>Schedules page</Header3>
<Shortcut name="New schedule">
@@ -1,4 +1,5 @@
import type { OutputColumnMetadata } from "@internal/clickhouse";
import { IconSortAscending, IconSortDescending } from "@tabler/icons-react";
import { BarChart, CheckIcon, LineChart, Plus, XIcon } from "lucide-react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { cn } from "~/utils/cn";
@@ -6,6 +7,7 @@ import { Paragraph } from "../primitives/Paragraph";
import { Popover, PopoverContent, PopoverTrigger } from "../primitives/Popover";
import { Select, SelectItem } from "../primitives/Select";
import { Switch } from "../primitives/Switch";
import SegmentedControl from "../primitives/SegmentedControl";
import { Button } from "../primitives/Buttons";
import {
type AggregationType,
@@ -234,54 +236,38 @@ export function ChartConfigPanel({ columns, config, onChange, className }: Chart
}
return (
<div className={cn("flex flex-col gap-2 p-2", className)}>
<div className={cn("flex flex-col gap-3 p-2", className)}>
{/* Chart Type */}
<div className="flex flex-col gap-3">
<ConfigField label="Type">
<div className="flex items-center">
<Button
type="button"
variant="tertiary/small"
className={cn(
"rounded-r-none border-b pl-1 pr-2",
config.chartType === "bar" ? "border-indigo-500" : "border-transparent"
)}
iconSpacing="gap-x-1"
onClick={() => updateConfig({ chartType: "bar" })}
LeadingIcon={BarChart}
leadingIconClassName={
config.chartType === "bar" ? "text-indigo-500" : "text-text-dimmed"
}
>
<span className={config.chartType === "bar" ? "text-indigo-500" : "text-text-dimmed"}>
Bar
</span>
</Button>
<Button
type="button"
variant="tertiary/small"
className={cn(
"rounded-l-none border-b pl-1 pr-2",
config.chartType === "line" ? "border-indigo-500" : "border-transparent"
)}
iconSpacing="gap-x-1"
onClick={() => updateConfig({ chartType: "line" })}
LeadingIcon={LineChart}
leadingIconClassName={
config.chartType === "line" ? "text-indigo-500" : "text-text-dimmed"
}
>
<span
className={config.chartType === "line" ? "text-indigo-500" : "text-text-dimmed"}
>
Line
</span>
</Button>
</div>
<SegmentedControl
name="chartType"
value={config.chartType}
variant="secondary/small"
options={[
{
label: (
<span className="flex items-center gap-1">
<BarChart className="size-3" /> Bar
</span>
),
value: "bar",
},
{
label: (
<span className="flex items-center gap-1">
<LineChart className="size-3" /> Line
</span>
),
value: "line",
},
]}
onChange={(value) => updateConfig({ chartType: value as "bar" | "line" })}
/>
</ConfigField>
</div>
<div className="flex flex-col gap-2">
<div className="flex flex-col gap-3">
{/* X-Axis */}
<ConfigField label="X-Axis">
<Select
@@ -529,9 +515,29 @@ export function ChartConfigPanel({ columns, config, onChange, className }: Chart
{/* Sort Direction (only when sorting) */}
{config.sortByColumn && (
<ConfigField label="Sort direction">
<SortDirectionToggle
direction={config.sortDirection}
onChange={(direction) => updateConfig({ sortDirection: direction })}
<SegmentedControl
name="sortDirection"
value={config.sortDirection}
variant="secondary/small"
options={[
{
label: (
<span className="flex items-center gap-1">
<IconSortAscending className="size-3" /> Asc
</span>
),
value: "asc",
},
{
label: (
<span className="flex items-center gap-1">
<IconSortDescending className="size-3" /> Desc
</span>
),
value: "desc",
},
]}
onChange={(value) => updateConfig({ sortDirection: value as SortDirection })}
/>
</ConfigField>
)}
@@ -543,51 +549,12 @@ export function ChartConfigPanel({ columns, config, onChange, className }: Chart
function ConfigField({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div className="flex flex-col gap-1">
{label && <span className="text-xs text-text-dimmed">{label}</span>}
{label && <span className="text-xs text-text-bright">{label}</span>}
{children}
</div>
);
}
function SortDirectionToggle({
direction,
onChange,
}: {
direction: SortDirection;
onChange: (direction: SortDirection) => void;
}) {
return (
<div className="flex gap-1">
<button
type="button"
onClick={() => onChange("asc")}
className={cn(
"rounded px-2 py-1 text-xs transition-colors",
direction === "asc"
? "bg-charcoal-700 text-text-bright"
: "text-text-dimmed hover:bg-charcoal-800 hover:text-text-bright"
)}
title="Ascending"
>
Asc
</button>
<button
type="button"
onClick={() => onChange("desc")}
className={cn(
"rounded px-2 py-1 text-xs transition-colors",
direction === "desc"
? "bg-charcoal-700 text-text-bright"
: "text-text-dimmed hover:bg-charcoal-800 hover:text-text-bright"
)}
title="Descending"
>
Desc
</button>
</div>
);
}
function SeriesColorPicker({
color,
onColorChange,
@@ -1,8 +1,9 @@
import type { OutputColumnMetadata } from "@internal/clickhouse";
import { BarChart3, LineChart } from "lucide-react";
import { memo, useMemo } from "react";
import type { ChartConfig } from "~/components/primitives/charts/Chart";
import { Chart } from "~/components/primitives/charts/ChartCompound";
import { Paragraph } from "../primitives/Paragraph";
import { ChartBlankState } from "../primitives/charts/ChartBlankState";
import type { AggregationType, ChartConfiguration } from "../metrics/QueryWidget";
import { aggregateValues } from "../primitives/charts/aggregation";
import { getRunStatusHexColor } from "~/components/runs/v3/TaskRunStatus";
@@ -947,20 +948,22 @@ export const QueryResultsChart = memo(function QueryResultsChart({
}, [isDateBased, xAxisTickFormatter, xAxisAngle]);
// Validation — all hooks must be above this point
const chartIcon = chartType === "bar" ? BarChart3 : LineChart;
if (!xAxisColumn) {
return <EmptyState message="Select an X-axis column to display the chart" />;
return <ChartBlankState icon={chartIcon} message="Select an X-axis column to display the chart" />;
}
if (yAxisColumns.length === 0) {
return <EmptyState message="Select a Y-axis column to display the chart" />;
return <ChartBlankState icon={chartIcon} message="Select a Y-axis column to display the chart" />;
}
if (rows.length === 0) {
return <EmptyState message="No data to display" />;
return <ChartBlankState icon={chartIcon} message="No data to display" />;
}
if (data.length === 0) {
return <EmptyState message="Unable to transform data for chart" />;
return <ChartBlankState icon={chartIcon} message="Unable to transform data for chart" />;
}
// Base x-axis props shared by all chart types
@@ -1113,12 +1116,3 @@ function createYAxisFormatter(data: Record<string, unknown>[], series: string[])
};
}
function EmptyState({ message }: { message: string }) {
return (
<div className="flex h-full min-h-[300px] items-center justify-center">
<Paragraph variant="small" className="text-text-dimmed">
{message}
</Paragraph>
</div>
);
}
@@ -1,6 +1,6 @@
import { ChevronDownIcon, ChevronUpDownIcon, ChevronUpIcon } from "@heroicons/react/20/solid";
import type { OutputColumnMetadata } from "@internal/clickhouse";
import { IconFilter2, IconFilter2X } from "@tabler/icons-react";
import { IconFilter2, IconFilter2X, IconTable } from "@tabler/icons-react";
import { rankItem } from "@tanstack/match-sorter-utils";
import {
flexRender,
@@ -19,7 +19,7 @@ import {
} from "@tanstack/react-table";
import { useVirtualizer } from "@tanstack/react-virtual";
import { formatDurationMilliseconds, MachinePresetName } from "@trigger.dev/core/v3";
import { ClipboardCheckIcon, ClipboardIcon } from "lucide-react";
import { AlertCircle, ClipboardCheckIcon, ClipboardIcon } from "lucide-react";
import { forwardRef, memo, useEffect, useMemo, useRef, useState } from "react";
import { EnvironmentLabel, EnvironmentSlug } from "~/components/environments/EnvironmentLabel";
import { MachineLabelCombo } from "~/components/MachineLabelCombo";
@@ -37,7 +37,9 @@ import { useProject } from "~/hooks/useProject";
import { cn } from "~/utils/cn";
import { formatCurrencyAccurate, formatNumber } from "~/utils/numberFormatter";
import { v3ProjectPath, v3RunPathFromFriendlyId } from "~/utils/pathBuilder";
import { ChartBlankState } from "../primitives/charts/ChartBlankState";
import { Paragraph } from "../primitives/Paragraph";
import { TextLink } from "../primitives/TextLink";
import { InfoIconTooltip, SimpleTooltip } from "../primitives/Tooltip";
import { QueueName } from "../runs/v3/QueueName";
@@ -412,7 +414,7 @@ function CellValueWrapper({
return (
<span
className="flex-1"
className="flex flex-1 items-center"
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
@@ -718,15 +720,16 @@ function CopyableCell({
return (
<div
className={cn(
"relative flex w-full items-center overflow-hidden px-2 py-1.5",
"relative flex h-full w-full items-center overflow-hidden px-2",
"bg-background-bright group-hover/row:bg-charcoal-750",
"font-mono text-xs text-text-dimmed group-hover/row:text-text-bright",
"[&_a:focus-visible]:underline [&_a:focus-visible]:underline-offset-[3px] [&_a:focus-visible]:outline-none",
alignment === "right" && "justify-end"
)}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<span className="flex h-4 items-center truncate">{children}</span>
<span className="flex items-center truncate">{children}</span>
{isHovered && (
<span
onClick={(e) => {
@@ -847,7 +850,7 @@ function HeaderCellContent({
}}
onMouseEnter={() => setIsFilterHovered(true)}
onMouseLeave={() => setIsFilterHovered(false)}
className="flex-shrink-0 rounded text-text-dimmed transition-colors hover:text-text-bright"
className="flex-shrink-0 rounded text-text-dimmed transition-colors focus-custom hover:text-text-bright"
title="Toggle column filters"
>
{showFilters ? <IconFilter2X className="size-4" /> : <IconFilter2 className="size-4" />}
@@ -903,11 +906,14 @@ export const TSQLResultsTable = memo(function TSQLResultsTable({
columns,
prettyFormatting = true,
sorting: defaultSorting = [],
showHeaderOnEmpty = false,
}: {
rows: Record<string, unknown>[];
columns: OutputColumnMetadata[];
prettyFormatting?: boolean;
sorting?: SortingState;
/** When true, show column headers + "No results" on empty data. When false, show a blank state icon. */
showHeaderOnEmpty?: boolean;
}) {
const tableContainerRef = useRef<HTMLDivElement>(null);
@@ -977,6 +983,10 @@ export const TSQLResultsTable = memo(function TSQLResultsTable({
// Empty state
if (rows.length === 0) {
if (!showHeaderOnEmpty) {
return <ChartBlankState icon={IconTable} message="No data to display" />;
}
return (
<div
className="h-full min-h-0 w-full overflow-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600"
@@ -984,7 +994,7 @@ export const TSQLResultsTable = memo(function TSQLResultsTable({
>
<table style={{ display: "grid" }}>
<thead
className="border-t border-grid-bright bg-background-bright"
className="border-t border-grid-bright bg-background-bright after:absolute after:bottom-0 after:left-0 after:right-0 after:h-px after:bg-grid-bright"
style={{
display: "grid",
position: "sticky",
@@ -1005,63 +1015,24 @@ export const TSQLResultsTable = memo(function TSQLResultsTable({
width: header.getSize(),
}}
>
<HeaderCellContent
alignment={meta?.alignment ?? "left"}
tooltip={meta?.outputColumn.description}
onFilterClick={() => {
if (!showFilters) {
setFocusFilterColumn(header.id);
} else {
setColumnFilters([]);
}
setShowFilters(!showFilters);
}}
showFilters={showFilters}
hasActiveFilter={!!header.column.getFilterValue()}
sortDirection={header.column.getIsSorted()}
onSortClick={header.column.getToggleSortingHandler()}
canSort={header.column.getCanSort()}
>
<HeaderCellContent alignment={meta?.alignment ?? "left"}>
{flexRender(header.column.columnDef.header, header.getContext())}
</HeaderCellContent>
{/* Column resizer */}
<div
onDoubleClick={() => header.column.resetSize()}
onMouseDown={header.getResizeHandler()}
onTouchStart={header.getResizeHandler()}
className={cn(
"absolute right-0 top-0 h-full w-0.5 cursor-col-resize touch-none select-none",
"opacity-0 group-hover/header:opacity-100",
"bg-charcoal-600 hover:bg-indigo-500",
header.column.getIsResizing() && "bg-indigo-500 opacity-100"
)}
/>
</th>
);
})}
</tr>
))}
{/* Filter row - shown when filters are toggled */}
{showFilters && (
<tr style={{ display: "flex", width: "100%" }}>
{table.getHeaderGroups()[0]?.headers.map((header) => (
<FilterCell
key={`filter-${header.id}`}
column={header.column}
width={header.getSize()}
shouldFocus={focusFilterColumn === header.id}
onFocused={() => setFocusFilterColumn(null)}
/>
))}
</tr>
)}
</thead>
<tbody className="border-b border-grid-bright" style={{ display: "grid" }}>
<tr style={{ display: "flex" }}>
<td>
<Paragraph variant="extra-small" className="p-4 text-text-dimmed">
No results
</Paragraph>
<tbody style={{ display: "grid" }}>
<tr style={{ display: "flex", width: "100%" }}>
<td className="w-full px-3 py-6" colSpan={columns.length}>
<div className="flex items-center justify-center gap-1.5">
<AlertCircle className="size-5 text-text-dimmed/50" />
<Paragraph variant="small" className="text-text-dimmed">
This query returned no results
</Paragraph>
</div>
</td>
</tr>
</tbody>
@@ -1171,6 +1142,7 @@ export const TSQLResultsTable = memo(function TSQLResultsTable({
position: "absolute",
transform: `translateY(${virtualRow.start}px)`,
width: "100%",
height: `${virtualRow.size}px`,
}}
>
{row.getVisibleCells().map((cell) => {
@@ -6,10 +6,12 @@ import { DialogClose } from "@radix-ui/react-dialog";
import { IconBraces, IconChartHistogram, IconFileTypeCsv } from "@tabler/icons-react";
import { assertNever } from "assert-never";
import { Maximize2 } from "lucide-react";
import { useCallback, useState, type ReactNode } from "react";
import { useCallback, useRef, useState, type ReactNode } from "react";
import { z } from "zod";
import { Card } from "~/components/primitives/charts/Card";
import { ShortcutKey } from "~/components/primitives/ShortcutKey";
import { SimpleTooltip } from "~/components/primitives/Tooltip";
import { useShortcutKeys } from "~/hooks/useShortcutKeys";
import { cn } from "~/utils/cn";
import { rowsToCSV, rowsToJSON } from "~/utils/dataExport";
import { QueryResultsChart } from "../code/QueryResultsChart";
@@ -142,6 +144,8 @@ export type QueryWidgetProps = {
accessory?: ReactNode;
isResizing?: boolean;
isDraggable?: boolean;
/** Additional className applied to the Card wrapper */
className?: string;
/** Callback when edit is clicked. Receives the current data. */
onEdit?: (data: QueryWidgetData) => void;
/** Callback when rename is clicked. Receives the new title. */
@@ -150,6 +154,8 @@ export type QueryWidgetProps = {
onDelete?: () => void;
/** Callback when duplicate is clicked. Receives the current data. */
onDuplicate?: (data: QueryWidgetData) => void;
/** When true, show table column headers even when there are no rows */
showTableHeaderOnEmpty?: boolean;
};
export function QueryWidget({
@@ -161,6 +167,7 @@ export function QueryWidget({
error,
isResizing,
isDraggable,
className,
onEdit,
onRename,
onDelete,
@@ -171,10 +178,21 @@ export function QueryWidget({
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false);
const [renameValue, setRenameValue] = useState(titleString ?? "");
const containerRef = useRef<HTMLDivElement>(null);
const hasEditActions = onEdit || onRename || onDelete || onDuplicate;
const hasData = props.data.rows.length > 0;
// "v" to toggle fullscreen on hovered widget
useShortcutKeys({
shortcut: { key: "v" },
action: useCallback(() => {
const isHovered = containerRef.current?.matches(":hover");
if (!isFullscreen && !isHovered) return;
setIsFullscreen((prev) => !prev);
}, [isFullscreen]),
});
const copyToClipboard = useCallback((text: string) => {
navigator.clipboard.writeText(text);
}, []);
@@ -194,8 +212,8 @@ export function QueryWidget({
}, [props.data, copyToClipboard]);
return (
<div className="group h-full">
<Card className="h-full overflow-hidden px-0 pb-0">
<div ref={containerRef} className="group h-full">
<Card className={cn("h-full overflow-hidden px-0 pb-0", className)}>
<Card.Header draggable={isDraggable}>
<div className="flex items-center gap-1.5">{title}</div>
<Card.Accessory>
@@ -211,10 +229,14 @@ export function QueryWidget({
/>
</span>
}
content="Maximize"
content={
<span className="flex items-center gap-1">
Maximize
<ShortcutKey shortcut={{ key: "v" }} variant="small/bright" />
</span>
}
asChild
/>
{accessory}
<Popover open={isMenuOpen} onOpenChange={setIsMenuOpen}>
<PopoverVerticalEllipseTrigger
isOpen={isMenuOpen}
@@ -307,6 +329,7 @@ export function QueryWidget({
</div>
</PopoverContent>
</Popover>
{accessory}
</Card.Accessory>
</Card.Header>
<LoadingBarDivider isLoading={isLoading ?? false} className="bg-transparent" />
@@ -382,6 +405,7 @@ type QueryWidgetBodyProps = {
isFullscreen: boolean;
setIsFullscreen: (open: boolean) => void;
isLoading: boolean;
showTableHeaderOnEmpty?: boolean;
};
function QueryWidgetBody({
@@ -392,6 +416,7 @@ function QueryWidgetBody({
isFullscreen,
setIsFullscreen,
isLoading,
showTableHeaderOnEmpty,
}: QueryWidgetBodyProps) {
const type = config.type;
@@ -410,6 +435,7 @@ function QueryWidgetBody({
columns={data.columns}
prettyFormatting={config.prettyFormatting}
sorting={config.sorting}
showHeaderOnEmpty={showTableHeaderOnEmpty}
/>
<Dialog open={isFullscreen} onOpenChange={setIsFullscreen}>
<DialogContent
@@ -423,6 +449,7 @@ function QueryWidgetBody({
columns={data.columns}
prettyFormatting={config.prettyFormatting}
sorting={config.sorting}
showHeaderOnEmpty={showTableHeaderOnEmpty}
/>
</div>
</DialogContent>
@@ -1,6 +1,6 @@
import { DialogClose } from "@radix-ui/react-dialog";
import { useFetcher, useNavigate } from "@remix-run/react";
import { IconChartHistogram } from "@tabler/icons-react";
import { IconCheck } from "@tabler/icons-react";
import { useEffect, useState } from "react";
import { useEnvironment } from "~/hooks/useEnvironment";
import {
@@ -138,7 +138,11 @@ export function SaveToDashboardDialog({
: "text-text-dimmed hover:bg-charcoal-750 hover:text-text-bright"
)}
>
<IconChartHistogram className="size-4 shrink-0 text-text-dimmed" />
{selectedDashboardId === dashboard.friendlyId ? (
<IconCheck className="size-4 shrink-0 text-green-500" />
) : (
<span className="size-4 shrink-0" />
)}
<span className="flex-1 truncate">{dashboard.title}</span>
<span
className={cn(
@@ -51,6 +51,7 @@ const ClientTabs = React.forwardRef<
<ClientTabsContext.Provider value={contextValue}>
<TabsPrimitive.Root
ref={ref}
activationMode="manual"
onValueChange={handleValueChange}
{...controlledProps}
{...props}
@@ -96,6 +97,7 @@ const ClientTabsTrigger = React.forwardRef<
return (
<TabsPrimitive.Trigger
ref={ref}
tabIndex={0}
className={cn(
"group relative flex h-full grow items-center justify-center focus-custom disabled:pointer-events-none disabled:opacity-50",
"flex-1 basis-0",
@@ -134,6 +136,7 @@ const ClientTabsTrigger = React.forwardRef<
return (
<TabsPrimitive.Trigger
ref={ref}
tabIndex={0}
className={cn(
"group flex flex-col items-center pt-1 focus-custom disabled:pointer-events-none disabled:opacity-50",
className
@@ -143,7 +146,7 @@ const ClientTabsTrigger = React.forwardRef<
<span
className={cn(
"text-sm transition duration-200",
isActive ? "text-text-bright" : "text-text-dimmed hover:text-text-bright"
isActive ? "text-text-bright" : "text-text-dimmed group-hover:text-text-bright"
)}
>
{children}
@@ -170,8 +173,9 @@ const ClientTabsTrigger = React.forwardRef<
return (
<TabsPrimitive.Trigger
ref={ref}
tabIndex={0}
className={cn(
"ring-offset-background focus-visible:ring-ring inline-flex items-center justify-center whitespace-nowrap border-r border-charcoal-700 px-2 text-sm transition-all first:pl-0 last:border-none data-[state=active]:text-indigo-500 data-[state=inactive]:text-text-dimmed data-[state=inactive]:hover:text-text-bright focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
"inline-flex items-center justify-center whitespace-nowrap border-r border-charcoal-700 px-2 text-sm transition-all first:pl-0 last:border-none focus-custom data-[state=active]:text-indigo-500 data-[state=inactive]:text-text-dimmed data-[state=inactive]:hover:text-text-bright disabled:pointer-events-none disabled:opacity-50",
className
)}
{...props}
@@ -188,8 +192,9 @@ const ClientTabsContent = React.forwardRef<
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
tabIndex={-1}
className={cn(
"ring-offset-background focus-visible:ring-ring mt-1 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2",
"mt-1 outline-none",
className,
"data-[state=inactive]:hidden"
)}
@@ -1,5 +1,6 @@
import { RadioGroup } from "@headlessui/react";
import { motion } from "framer-motion";
import type { ReactNode } from "react";
import { cn } from "~/utils/cn";
const sizes = {
@@ -63,7 +64,7 @@ const variants = {
type VariantType = keyof typeof variants;
type Options = {
label: string;
label: ReactNode;
value: string;
};
@@ -8,12 +8,15 @@ import { cn } from "~/utils/cn";
import { useOperatingSystem } from "./OperatingSystemProvider";
import { KeyboardEnterIcon } from "~/assets/icons/KeyboardEnterIcon";
const small =
"justify-center text-[0.6rem] font-mono font-medium min-w-[1rem] min-h-[1rem] rounded-[2px] tabular-nums px-1 ml-1 -mr-0.5 flex items-center gap-x-1 border transition uppercase";
const medium =
"justify-center min-w-[1.25rem] min-h-[1.25rem] text-[0.65rem] font-mono font-medium rounded-[2px] tabular-nums px-1 ml-1 -mr-0.5 flex items-center gap-x-1.5 border border-dimmed/40 text-text-dimmed group-hover:text-text-bright/80 group-hover:border-dimmed/60 transition uppercase";
export const variants = {
small:
"justify-center text-[0.6rem] font-mono font-medium min-w-[1rem] min-h-[1rem] rounded-[2px] tabular-nums px-1 ml-1 -mr-0.5 flex items-center gap-x-1 border border-text-dimmed/40 text-text-dimmed group-hover:text-text-bright/80 group-hover:border-text-dimmed/60 transition uppercase",
small: cn(small, "border-text-dimmed/40 text-text-dimmed group-hover:text-text-bright/80 group-hover:border-text-dimmed/60"),
"small/bright": cn(small, "bg-charcoal-750 text-text-bright border-charcoal-650"),
medium: cn(medium, "group-hover:border-charcoal-550"),
"medium/bright": cn(medium, "bg-charcoal-750 text-text-bright border-charcoal-650"),
};
@@ -54,10 +57,10 @@ export function ShortcutKey({ shortcut, variant, className }: ShortcutKeyProps)
);
}
function keyString(key: string, isMac: boolean, variant: "small" | "medium" | "medium/bright") {
function keyString(key: string, isMac: boolean, variant: ShortcutKeyVariant) {
key = key.toLowerCase();
const className = variant === "small" ? "w-2.5 h-4" : "w-2.5 h-4.5";
const className = variant.startsWith("small") ? "w-2.5 h-4" : "w-2.5 h-4.5";
switch (key) {
case "enter":
@@ -86,9 +89,9 @@ function keyString(key: string, isMac: boolean, variant: "small" | "medium" | "m
function modifierString(
modifier: Modifier,
isMac: boolean,
variant: "small" | "medium" | "medium/bright"
variant: ShortcutKeyVariant
): string | JSX.Element {
const className = variant === "small" ? "w-2.5 h-4" : "w-3.5 h-5";
const className = variant.startsWith("small") ? "w-2.5 h-4" : "w-3.5 h-5";
switch (modifier) {
case "alt":
@@ -1,12 +1,13 @@
import type { OutputColumnMetadata } from "@internal/tsql";
import { Hash } from "lucide-react";
import { useMemo } from "react";
import type {
BigNumberAggregationType,
BigNumberConfiguration,
} from "~/components/metrics/QueryWidget";
import { AnimatedNumber } from "../AnimatedNumber";
import { ChartBlankState } from "./ChartBlankState";
import { Spinner } from "../Spinner";
import { Paragraph } from "../Paragraph";
interface BigNumberCardProps {
rows: Record<string, unknown>[];
@@ -138,13 +139,7 @@ export function BigNumberCard({ rows, columns, config, isLoading = false }: BigN
}
if (result === null) {
return (
<div className="grid h-full place-items-center [container-type:size]">
<Paragraph variant="small" className="text-text-dimmed">
No data to display
</Paragraph>
</div>
);
return <ChartBlankState icon={Hash} message="No data to display" />;
}
const { displayValue, unitSuffix, decimalPlaces } = abbreviate
@@ -15,17 +15,11 @@ export const Card = ({ children, className }: { children: ReactNode; className?:
);
};
const CardHeader = ({
children,
draggable,
}: {
children: ReactNode;
draggable?: boolean;
}) => {
const CardHeader = ({ children, draggable }: { children: ReactNode; draggable?: boolean }) => {
return (
<Header3
className={cn(
"drag-handle mb-3 flex items-center justify-between gap-2 px-3",
"drag-handle mb-3 flex items-center justify-between gap-2 pl-4 pr-3",
draggable && "cursor-grab active:cursor-grabbing"
)}
>
@@ -1,4 +1,4 @@
import React, { useCallback, useMemo } from "react";
import React, { useCallback } from "react";
import {
Bar,
BarChart,
@@ -11,20 +11,13 @@ import {
type XAxisProps,
type YAxisProps,
} from "recharts";
import {
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
type ChartState,
} from "~/components/primitives/charts/Chart";
import { cn } from "~/utils/cn";
import { ChartBarLoading, ChartBarInvalid, ChartBarNoData } from "./ChartLoading";
import { ChartTooltip, ChartTooltipContent } from "~/components/primitives/charts/Chart";
import { useChartContext } from "./ChartContext";
import { ChartRoot, useHasNoData } from "./ChartRoot";
import { ChartBarInvalid, ChartBarLoading, ChartBarNoData } from "./ChartLoading";
import { useHasNoData } from "./ChartRoot";
// Legend is now rendered by ChartRoot outside the chart container
import { ZoomTooltip, useZoomHandlers } from "./ChartZoom";
import { getBarOpacity } from "./hooks/useHighlightState";
import type { ZoomRange } from "./hooks/useZoomSelection";
//TODO: fix the first and last bars in a stack not having rounded corners
@@ -0,0 +1,23 @@
import { cn } from "~/utils/cn";
import { Paragraph } from "../Paragraph";
export function ChartBlankState({
icon: Icon,
message,
className,
}: {
icon?: React.ComponentType<{ className?: string }>;
message: string;
className?: string;
}) {
return (
<div className={cn("flex h-full w-full items-center justify-center", className)}>
<div className="-mt-3 flex flex-col items-center gap-2">
{Icon && <Icon className="size-12 text-charcoal-700" />}
<Paragraph variant="small" className="text-text-dimmed/70">
{message}
</Paragraph>
</div>
</div>
);
}
@@ -167,7 +167,7 @@ export function ChartLegendCompound({
return (
<div
className={cn("flex flex-col pt-4 text-sm", scrollable && "max-h-[50%] min-h-0", className)}
className={cn("flex flex-col px-2 pb-2 pt-4 text-sm", scrollable && "max-h-[50%] min-h-0", className)}
>
{/* Total row */}
<div
@@ -507,9 +507,7 @@ export function QueryEditor({
const isLoading = fetcher.state === "submitting" || fetcher.state === "loading";
// Stable string key of result column names (types excluded — they can vary between runs)
const columnNamesKey = results?.columns
? results.columns.map((c) => c.name).join(",")
: "";
const columnNamesKey = results?.columns ? results.columns.map((c) => c.name).join(",") : "";
// Use a ref so the effect can read chartConfig without re-firing on every config tweak
const chartConfigRef = useRef(chartConfig);
@@ -773,7 +771,7 @@ export function QueryEditor({
</div>
) : results?.rows && results?.columns ? (
<div
className={`grid h-full max-h-full overflow-hidden bg-charcoal-900 ${
className={`grid h-full max-h-full overflow-hidden bg-background-bright ${
hasQueryResultsCallouts(results.hiddenColumns, results.periodClipped)
? "grid-rows-[auto_1fr]"
: "grid-rows-[1fr]"
@@ -784,8 +782,10 @@ export function QueryEditor({
periodClipped={results.periodClipped}
organizationSlug={organization.slug}
/>
<div className="overflow-hidden p-2">
<div className="overflow-hidden">
<QueryWidget
className="border-0"
showTableHeaderOnEmpty
title={
<QueryTitle
isTitleLoading={isTitleLoading}
@@ -829,7 +829,7 @@ export function QueryEditor({
</ClientTabsContent>
<ClientTabsContent
value="graph"
className={`m-0 grid h-full max-h-full min-h-0 overflow-hidden bg-charcoal-900 ${
className={`m-0 grid h-full max-h-full min-h-0 overflow-hidden bg-background-bright ${
results?.rows &&
results.rows.length > 0 &&
hasQueryResultsCallouts(results.hiddenColumns, results.periodClipped)
@@ -878,7 +878,7 @@ export function QueryEditor({
</ClientTabsContent>
<ClientTabsContent
value="bignumber"
className={`m-0 grid h-full max-h-full min-h-0 overflow-hidden bg-charcoal-900 ${
className={`m-0 grid h-full max-h-full min-h-0 overflow-hidden bg-background-bright ${
results?.rows &&
results.rows.length > 0 &&
hasQueryResultsCallouts(results.hiddenColumns, results.periodClipped)
@@ -1219,8 +1219,9 @@ function ResultsChart({
<>
<ResizablePanelGroup className="overflow-hidden">
<ResizablePanel id="chart-results">
<div className="h-full overflow-hidden bg-charcoal-900 p-2">
<div className="h-full overflow-hidden bg-background-bright">
<QueryWidget
className="border-0"
title={
<QueryTitle
isTitleLoading={isTitleLoading}
@@ -1297,8 +1298,9 @@ function ResultsBigNumber({
<>
<ResizablePanelGroup className="overflow-hidden">
<ResizablePanel id="bignumber-results">
<div className="h-full overflow-hidden bg-charcoal-900 p-2">
<div className="h-full overflow-hidden bg-background-bright">
<QueryWidget
className="border-0"
title={
<QueryTitle
isTitleLoading={isTitleLoading}
@@ -1367,7 +1369,7 @@ function BigNumberConfigPanel({
<div className="flex h-full flex-col gap-2 overflow-y-auto p-2">
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-1">
<Paragraph variant="extra-small" className="text-text-dimmed">
<Paragraph variant="extra-small" className="text-text-bright">
Column
</Paragraph>
<Select
@@ -1389,7 +1391,7 @@ function BigNumberConfigPanel({
</Select>
</div>
<div className="flex flex-col gap-1">
<Paragraph variant="extra-small" className="text-text-dimmed">
<Paragraph variant="extra-small" className="text-text-bright">
Sort order
</Paragraph>
<Select
@@ -1417,7 +1419,7 @@ function BigNumberConfigPanel({
</Select>
</div>
<div className="flex flex-col gap-1">
<Paragraph variant="extra-small" className="text-text-dimmed">
<Paragraph variant="extra-small" className="text-text-bright">
Aggregation
</Paragraph>
<Select
@@ -1451,7 +1453,7 @@ function BigNumberConfigPanel({
/>
</div>
<div className="flex flex-col gap-1">
<Paragraph variant="extra-small" className="text-text-dimmed">
<Paragraph variant="extra-small" className="text-text-bright">
Prefix
</Paragraph>
<Input
@@ -1462,7 +1464,7 @@ function BigNumberConfigPanel({
/>
</div>
<div className="flex flex-col gap-1">
<Paragraph variant="extra-small" className="text-text-dimmed">
<Paragraph variant="extra-small" className="text-text-bright">
Suffix
</Paragraph>
<Input
@@ -55,7 +55,7 @@ export function AITabContent({
key: (prev?.key ?? 0) + 1,
}));
}}
className="group flex w-fit items-center gap-2 rounded-full border border-dashed border-charcoal-600 px-4 py-2 transition-colors hover:border-solid hover:border-indigo-500"
className="group flex w-fit items-center gap-2 rounded-full border border-dashed border-charcoal-600 px-4 py-2 transition-colors hover:border-solid hover:border-indigo-500 focus-custom focus-visible:!rounded-full"
>
<SparkleListIcon className="size-4 shrink-0 text-text-dimmed transition group-hover:text-indigo-500" />
<Paragraph variant="small" className="text-left transition group-hover:text-text-bright">