From 98e715e9a5b42ebbbc3bada2e0ab2ae4b9f83e9f Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Fri, 13 Feb 2026 18:17:55 +0000 Subject: [PATCH 01/21] Move the Add to dashboard button to the right side of the chart --- apps/webapp/app/components/metrics/QueryWidget.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/webapp/app/components/metrics/QueryWidget.tsx b/apps/webapp/app/components/metrics/QueryWidget.tsx index 7e49259f83..4864e76e1b 100644 --- a/apps/webapp/app/components/metrics/QueryWidget.tsx +++ b/apps/webapp/app/components/metrics/QueryWidget.tsx @@ -214,7 +214,6 @@ export function QueryWidget({ content="Maximize" asChild /> - {accessory} + {accessory} From d5aaba13b99f67a5685c63f92c9ccba8e07c8fac Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Fri, 13 Feb 2026 18:23:32 +0000 Subject: [PATCH 02/21] Show a checkmark on the selected dashboard when choosing from the modal --- .../app/components/metrics/SaveToDashboardDialog.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/webapp/app/components/metrics/SaveToDashboardDialog.tsx b/apps/webapp/app/components/metrics/SaveToDashboardDialog.tsx index 1e2c1232eb..828ff2b911 100644 --- a/apps/webapp/app/components/metrics/SaveToDashboardDialog.tsx +++ b/apps/webapp/app/components/metrics/SaveToDashboardDialog.tsx @@ -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" )} > - + {selectedDashboardId === dashboard.friendlyId ? ( + + ) : ( + + )} {dashboard.title} Date: Sat, 14 Feb 2026 10:19:47 +0000 Subject: [PATCH 03/21] =?UTF-8?q?Remove=20the=20=E2=80=98card=E2=80=99=20s?= =?UTF-8?q?tyle=20from=20the=20Query=20page=20so=20the=20sections=20are=20?= =?UTF-8?q?clearer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/components/metrics/QueryWidget.tsx | 5 ++++- .../app/components/query/QueryEditor.tsx | 19 ++++++++++--------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/apps/webapp/app/components/metrics/QueryWidget.tsx b/apps/webapp/app/components/metrics/QueryWidget.tsx index 4864e76e1b..19641cc700 100644 --- a/apps/webapp/app/components/metrics/QueryWidget.tsx +++ b/apps/webapp/app/components/metrics/QueryWidget.tsx @@ -142,6 +142,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. */ @@ -161,6 +163,7 @@ export function QueryWidget({ error, isResizing, isDraggable, + className, onEdit, onRename, onDelete, @@ -195,7 +198,7 @@ export function QueryWidget({ return (
- +
{title}
diff --git a/apps/webapp/app/components/query/QueryEditor.tsx b/apps/webapp/app/components/query/QueryEditor.tsx index 3ffe889cc4..faf6aebc31 100644 --- a/apps/webapp/app/components/query/QueryEditor.tsx +++ b/apps/webapp/app/components/query/QueryEditor.tsx @@ -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({
) : results?.rows && results?.columns ? (
-
+
0 && hasQueryResultsCallouts(results.hiddenColumns, results.periodClipped) @@ -878,7 +877,7 @@ export function QueryEditor({ 0 && hasQueryResultsCallouts(results.hiddenColumns, results.periodClipped) @@ -1219,8 +1218,9 @@ function ResultsChart({ <> -
+
-
+
Date: Sat, 14 Feb 2026 11:33:57 +0000 Subject: [PATCH 04/21] Segmented control accepts react node --- apps/webapp/app/components/primitives/SegmentedControl.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/webapp/app/components/primitives/SegmentedControl.tsx b/apps/webapp/app/components/primitives/SegmentedControl.tsx index f87863a45b..98561e4122 100644 --- a/apps/webapp/app/components/primitives/SegmentedControl.tsx +++ b/apps/webapp/app/components/primitives/SegmentedControl.tsx @@ -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; }; From 4a41f68e67a9a4be335554c421d75e395b28eede Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Sat, 14 Feb 2026 11:34:30 +0000 Subject: [PATCH 05/21] Improves the graph query section side bar --- .../app/components/code/ChartConfigPanel.tsx | 71 ++++++++----------- 1 file changed, 28 insertions(+), 43 deletions(-) diff --git a/apps/webapp/app/components/code/ChartConfigPanel.tsx b/apps/webapp/app/components/code/ChartConfigPanel.tsx index d4b64682e3..4eb06fb957 100644 --- a/apps/webapp/app/components/code/ChartConfigPanel.tsx +++ b/apps/webapp/app/components/code/ChartConfigPanel.tsx @@ -6,6 +6,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 +235,38 @@ export function ChartConfigPanel({ columns, config, onChange, className }: Chart } return ( -
+
{/* Chart Type */}
-
- - -
+ + Bar + + ), + value: "bar", + }, + { + label: ( + + Line + + ), + value: "line", + }, + ]} + onChange={(value) => updateConfig({ chartType: value as "bar" | "line" })} + />
-
+
{/* X-Axis */}
- + Sort order
- + Prefix
- + Suffix Date: Sat, 14 Feb 2026 11:49:35 +0000 Subject: [PATCH 07/21] Import tidy --- .../app/components/primitives/charts/ChartBar.tsx | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/apps/webapp/app/components/primitives/charts/ChartBar.tsx b/apps/webapp/app/components/primitives/charts/ChartBar.tsx index a34ce66759..7ab3299e2b 100644 --- a/apps/webapp/app/components/primitives/charts/ChartBar.tsx +++ b/apps/webapp/app/components/primitives/charts/ChartBar.tsx @@ -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 From 355e5abfad9f2b8e90081a6e360dabd204cfe3e4 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Sat, 14 Feb 2026 11:49:46 +0000 Subject: [PATCH 08/21] Consistent chart title padding --- apps/webapp/app/components/primitives/charts/Card.tsx | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/apps/webapp/app/components/primitives/charts/Card.tsx b/apps/webapp/app/components/primitives/charts/Card.tsx index 9249832b5e..f4fb13b51b 100644 --- a/apps/webapp/app/components/primitives/charts/Card.tsx +++ b/apps/webapp/app/components/primitives/charts/Card.tsx @@ -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 ( From 1e21fdd4846a8f8d78bcae607c2219b33baa4564 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Sat, 14 Feb 2026 11:49:53 +0000 Subject: [PATCH 09/21] Nicer legend padding --- .../app/components/primitives/charts/ChartLegendCompound.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/webapp/app/components/primitives/charts/ChartLegendCompound.tsx b/apps/webapp/app/components/primitives/charts/ChartLegendCompound.tsx index 54d725a281..7e9cf2c879 100644 --- a/apps/webapp/app/components/primitives/charts/ChartLegendCompound.tsx +++ b/apps/webapp/app/components/primitives/charts/ChartLegendCompound.tsx @@ -167,7 +167,7 @@ export function ChartLegendCompound({ return (
{/* Total row */}
Date: Sat, 14 Feb 2026 11:55:12 +0000 Subject: [PATCH 10/21] Use our segmented control instead --- .../app/components/code/ChartConfigPanel.tsx | 66 +++++++------------ 1 file changed, 24 insertions(+), 42 deletions(-) diff --git a/apps/webapp/app/components/code/ChartConfigPanel.tsx b/apps/webapp/app/components/code/ChartConfigPanel.tsx index 4eb06fb957..3032795c1b 100644 --- a/apps/webapp/app/components/code/ChartConfigPanel.tsx +++ b/apps/webapp/app/components/code/ChartConfigPanel.tsx @@ -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"; @@ -514,9 +515,29 @@ export function ChartConfigPanel({ columns, config, onChange, className }: Chart {/* Sort Direction (only when sorting) */} {config.sortByColumn && ( - updateConfig({ sortDirection: direction })} + + Asc + + ), + value: "asc", + }, + { + label: ( + + Desc + + ), + value: "desc", + }, + ]} + onChange={(value) => updateConfig({ sortDirection: value as SortDirection })} /> )} @@ -534,45 +555,6 @@ function ConfigField({ label, children }: { label: string; children: React.React ); } -function SortDirectionToggle({ - direction, - onChange, -}: { - direction: SortDirection; - onChange: (direction: SortDirection) => void; -}) { - return ( -
- - -
- ); -} - function SeriesColorPicker({ color, onColorChange, From f67c13149c32a78a43f0b1a0c8ce0213545e2789 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Sat, 14 Feb 2026 14:19:31 +0000 Subject: [PATCH 11/21] New shortcut key style variant --- .../app/components/primitives/ShortcutKey.tsx | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/apps/webapp/app/components/primitives/ShortcutKey.tsx b/apps/webapp/app/components/primitives/ShortcutKey.tsx index 567cf68d61..72d77c70da 100644 --- a/apps/webapp/app/components/primitives/ShortcutKey.tsx +++ b/apps/webapp/app/components/primitives/ShortcutKey.tsx @@ -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": From 9fafcb5193ae96818907edbfd341bef19e2c1e43 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Sat, 14 Feb 2026 14:20:56 +0000 Subject: [PATCH 12/21] =?UTF-8?q?Adds=20=E2=80=9Cv=E2=80=9D=20shortcut=20t?= =?UTF-8?q?o=20quick=20view=20charts=20full=20screen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/components/metrics/QueryWidget.tsx | 40 +++++++++++++++++-- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/apps/webapp/app/components/metrics/QueryWidget.tsx b/apps/webapp/app/components/metrics/QueryWidget.tsx index 19641cc700..76b69838ac 100644 --- a/apps/webapp/app/components/metrics/QueryWidget.tsx +++ b/apps/webapp/app/components/metrics/QueryWidget.tsx @@ -6,9 +6,10 @@ 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, useEffect, 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 { cn } from "~/utils/cn"; import { rowsToCSV, rowsToJSON } from "~/utils/dataExport"; @@ -174,10 +175,38 @@ export function QueryWidget({ const [isMenuOpen, setIsMenuOpen] = useState(false); const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false); const [renameValue, setRenameValue] = useState(titleString ?? ""); + const containerRef = useRef(null); const hasEditActions = onEdit || onRename || onDelete || onDuplicate; const hasData = props.data.rows.length > 0; + // "v" to fullscreen the hovered widget + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (isRenameDialogOpen || isMenuOpen) return; + if (e.key !== "v" || e.metaKey || e.ctrlKey || e.altKey || e.shiftKey) return; + + // Ignore when typing in inputs/textareas/contenteditable + const target = e.target as HTMLElement; + if ( + target.tagName === "INPUT" || + target.tagName === "TEXTAREA" || + target.tagName === "SELECT" || + target.isContentEditable + ) { + return; + } + + // When not fullscreen, require hover to activate + if (!isFullscreen && !containerRef.current?.matches(":hover")) return; + + e.preventDefault(); + setIsFullscreen((prev) => !prev); + }; + document.addEventListener("keydown", handleKeyDown); + return () => document.removeEventListener("keydown", handleKeyDown); + }, [isFullscreen, isRenameDialogOpen, isMenuOpen]); + const copyToClipboard = useCallback((text: string) => { navigator.clipboard.writeText(text); }, []); @@ -197,7 +226,7 @@ export function QueryWidget({ }, [props.data, copyToClipboard]); return ( -
+
{title}
@@ -214,7 +243,12 @@ export function QueryWidget({ /> } - content="Maximize" + content={ + + Maximize + + + } asChild /> From 8df40ca37680ecc36a640987560adba09b43a955 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Sat, 14 Feb 2026 15:10:38 +0000 Subject: [PATCH 13/21] Use our useShortcutKeys component instead --- .../app/components/metrics/QueryWidget.tsx | 36 ++++++------------- 1 file changed, 10 insertions(+), 26 deletions(-) diff --git a/apps/webapp/app/components/metrics/QueryWidget.tsx b/apps/webapp/app/components/metrics/QueryWidget.tsx index 76b69838ac..f403857045 100644 --- a/apps/webapp/app/components/metrics/QueryWidget.tsx +++ b/apps/webapp/app/components/metrics/QueryWidget.tsx @@ -6,11 +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, useEffect, useRef, 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"; @@ -180,32 +181,15 @@ export function QueryWidget({ const hasEditActions = onEdit || onRename || onDelete || onDuplicate; const hasData = props.data.rows.length > 0; - // "v" to fullscreen the hovered widget - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - if (isRenameDialogOpen || isMenuOpen) return; - if (e.key !== "v" || e.metaKey || e.ctrlKey || e.altKey || e.shiftKey) return; - - // Ignore when typing in inputs/textareas/contenteditable - const target = e.target as HTMLElement; - if ( - target.tagName === "INPUT" || - target.tagName === "TEXTAREA" || - target.tagName === "SELECT" || - target.isContentEditable - ) { - return; - } - - // When not fullscreen, require hover to activate - if (!isFullscreen && !containerRef.current?.matches(":hover")) return; - - e.preventDefault(); + // "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); - }; - document.addEventListener("keydown", handleKeyDown); - return () => document.removeEventListener("keydown", handleKeyDown); - }, [isFullscreen, isRenameDialogOpen, isMenuOpen]); + }, [isFullscreen]), + }); const copyToClipboard = useCallback((text: string) => { navigator.clipboard.writeText(text); From 4c3e07348d009408a971007662d2a47f1ced098f Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Sat, 14 Feb 2026 15:11:16 +0000 Subject: [PATCH 14/21] Adds new full screen shortcut to shortcuts sheet --- apps/webapp/app/components/Shortcuts.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/webapp/app/components/Shortcuts.tsx b/apps/webapp/app/components/Shortcuts.tsx index df76bdc522..2decc82c91 100644 --- a/apps/webapp/app/components/Shortcuts.tsx +++ b/apps/webapp/app/components/Shortcuts.tsx @@ -193,6 +193,12 @@ function ShortcutContent() {
+
+ Metrics page + + + +
Schedules page From 392d666e6850431c1122b56fefc753bc0007cedc Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Sat, 14 Feb 2026 15:29:36 +0000 Subject: [PATCH 15/21] Improved accessability styles when tabbing through buttons --- apps/webapp/app/components/code/TSQLResultsTable.tsx | 3 ++- .../AITabContent.tsx | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/webapp/app/components/code/TSQLResultsTable.tsx b/apps/webapp/app/components/code/TSQLResultsTable.tsx index 951e848235..f7cd7d29be 100644 --- a/apps/webapp/app/components/code/TSQLResultsTable.tsx +++ b/apps/webapp/app/components/code/TSQLResultsTable.tsx @@ -721,6 +721,7 @@ function CopyableCell({ "relative flex w-full items-center overflow-hidden px-2 py-1.5", "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)} @@ -847,7 +848,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 hover:text-text-bright focus-custom" title="Toggle column filters" > {showFilters ? : } diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/AITabContent.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/AITabContent.tsx index 0f3d4042bf..999eab3e8f 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/AITabContent.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/AITabContent.tsx @@ -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" > From 31666211e45d1f4d55a9128105f6f06e2a8c2c7f Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Sat, 14 Feb 2026 15:48:54 +0000 Subject: [PATCH 16/21] More accessible tabbing improvements --- apps/webapp/app/components/primitives/ClientTabs.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/webapp/app/components/primitives/ClientTabs.tsx b/apps/webapp/app/components/primitives/ClientTabs.tsx index 737f37bcd1..dd5f7cd186 100644 --- a/apps/webapp/app/components/primitives/ClientTabs.tsx +++ b/apps/webapp/app/components/primitives/ClientTabs.tsx @@ -51,6 +51,7 @@ const ClientTabs = React.forwardRef< (({ className, ...props }, ref) => ( Date: Sat, 14 Feb 2026 15:57:47 +0000 Subject: [PATCH 17/21] small fix for hovering on a client tab --- apps/webapp/app/components/primitives/ClientTabs.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/webapp/app/components/primitives/ClientTabs.tsx b/apps/webapp/app/components/primitives/ClientTabs.tsx index dd5f7cd186..aef45fe0cc 100644 --- a/apps/webapp/app/components/primitives/ClientTabs.tsx +++ b/apps/webapp/app/components/primitives/ClientTabs.tsx @@ -146,7 +146,7 @@ const ClientTabsTrigger = React.forwardRef< {children} From 95443d2ea598de39d858969168189e52783eb324 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Sat, 14 Feb 2026 16:22:20 +0000 Subject: [PATCH 18/21] Fixes table hover state height --- apps/webapp/app/components/code/TSQLResultsTable.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/webapp/app/components/code/TSQLResultsTable.tsx b/apps/webapp/app/components/code/TSQLResultsTable.tsx index f7cd7d29be..c43a11e4b5 100644 --- a/apps/webapp/app/components/code/TSQLResultsTable.tsx +++ b/apps/webapp/app/components/code/TSQLResultsTable.tsx @@ -718,7 +718,7 @@ function CopyableCell({ return (
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} > - {children} + {children} {isHovered && ( { @@ -1172,6 +1172,7 @@ export const TSQLResultsTable = memo(function TSQLResultsTable({ position: "absolute", transform: `translateY(${virtualRow.start}px)`, width: "100%", + height: `${virtualRow.size}px`, }} > {row.getVisibleCells().map((cell) => { From ed640323a39b43f7b063617078c6ac4a81683097 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Sat, 14 Feb 2026 16:22:33 +0000 Subject: [PATCH 19/21] Center the status column --- apps/webapp/app/components/code/TSQLResultsTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/webapp/app/components/code/TSQLResultsTable.tsx b/apps/webapp/app/components/code/TSQLResultsTable.tsx index c43a11e4b5..d6bb0b8b76 100644 --- a/apps/webapp/app/components/code/TSQLResultsTable.tsx +++ b/apps/webapp/app/components/code/TSQLResultsTable.tsx @@ -412,7 +412,7 @@ function CellValueWrapper({ return ( setHovered(true)} onMouseLeave={() => setHovered(false)} > From 0462aa670c6c0b7c7a27bf103660b71e6a912bc6 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Sat, 14 Feb 2026 19:00:05 +0000 Subject: [PATCH 20/21] Nice chart blank states --- .../app/components/code/QueryResultsChart.tsx | 22 ++--- .../app/components/code/TSQLResultsTable.tsx | 99 +------------------ .../primitives/charts/BigNumberCard.tsx | 11 +-- .../primitives/charts/ChartBlankState.tsx | 23 +++++ 4 files changed, 39 insertions(+), 116 deletions(-) create mode 100644 apps/webapp/app/components/primitives/charts/ChartBlankState.tsx diff --git a/apps/webapp/app/components/code/QueryResultsChart.tsx b/apps/webapp/app/components/code/QueryResultsChart.tsx index 4f7899a740..c66db5a8f6 100644 --- a/apps/webapp/app/components/code/QueryResultsChart.tsx +++ b/apps/webapp/app/components/code/QueryResultsChart.tsx @@ -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 ; + return ; } if (yAxisColumns.length === 0) { - return ; + return ; } if (rows.length === 0) { - return ; + return ; } if (data.length === 0) { - return ; + return ; } // Base x-axis props shared by all chart types @@ -1113,12 +1116,3 @@ function createYAxisFormatter(data: Record[], series: string[]) }; } -function EmptyState({ message }: { message: string }) { - return ( -
- - {message} - -
- ); -} diff --git a/apps/webapp/app/components/code/TSQLResultsTable.tsx b/apps/webapp/app/components/code/TSQLResultsTable.tsx index d6bb0b8b76..62ca631593 100644 --- a/apps/webapp/app/components/code/TSQLResultsTable.tsx +++ b/apps/webapp/app/components/code/TSQLResultsTable.tsx @@ -1,6 +1,7 @@ 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 { ChartBlankState } from "../primitives/charts/ChartBlankState"; import { rankItem } from "@tanstack/match-sorter-utils"; import { flexRender, @@ -37,7 +38,7 @@ import { useProject } from "~/hooks/useProject"; import { cn } from "~/utils/cn"; import { formatCurrencyAccurate, formatNumber } from "~/utils/numberFormatter"; import { v3ProjectPath, v3RunPathFromFriendlyId } from "~/utils/pathBuilder"; -import { Paragraph } from "../primitives/Paragraph"; + import { TextLink } from "../primitives/TextLink"; import { InfoIconTooltip, SimpleTooltip } from "../primitives/Tooltip"; import { QueueName } from "../runs/v3/QueueName"; @@ -848,7 +849,7 @@ function HeaderCellContent({ }} onMouseEnter={() => setIsFilterHovered(true)} onMouseLeave={() => setIsFilterHovered(false)} - className="flex-shrink-0 rounded text-text-dimmed transition-colors hover:text-text-bright focus-custom" + className="flex-shrink-0 rounded text-text-dimmed transition-colors focus-custom hover:text-text-bright" title="Toggle column filters" > {showFilters ? : } @@ -978,97 +979,7 @@ export const TSQLResultsTable = memo(function TSQLResultsTable({ // Empty state if (rows.length === 0) { - return ( -
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - const meta = header.column.columnDef.meta as ColumnMeta | undefined; - return ( - - ); - })} - - ))} - {/* Filter row - shown when filters are toggled */} - {showFilters && ( - - {table.getHeaderGroups()[0]?.headers.map((header) => ( - setFocusFilterColumn(null)} - /> - ))} - - )} - - - - - - -
- { - 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()} - > - {flexRender(header.column.columnDef.header, header.getContext())} - - {/* Column resizer */} -
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" - )} - /> -
- - No results - -
-
- ); + return ; } return ( diff --git a/apps/webapp/app/components/primitives/charts/BigNumberCard.tsx b/apps/webapp/app/components/primitives/charts/BigNumberCard.tsx index f9d4280412..1ed3b50a64 100644 --- a/apps/webapp/app/components/primitives/charts/BigNumberCard.tsx +++ b/apps/webapp/app/components/primitives/charts/BigNumberCard.tsx @@ -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[]; @@ -138,13 +139,7 @@ export function BigNumberCard({ rows, columns, config, isLoading = false }: BigN } if (result === null) { - return ( -
- - No data to display - -
- ); + return ; } const { displayValue, unitSuffix, decimalPlaces } = abbreviate diff --git a/apps/webapp/app/components/primitives/charts/ChartBlankState.tsx b/apps/webapp/app/components/primitives/charts/ChartBlankState.tsx new file mode 100644 index 0000000000..9a093254e0 --- /dev/null +++ b/apps/webapp/app/components/primitives/charts/ChartBlankState.tsx @@ -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 ( +
+
+ {Icon && } + + {message} + +
+
+ ); +} From c61d39cdfe84010c767d3c4dbd9b4d8bdcb3b39f Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Sat, 14 Feb 2026 21:13:17 +0000 Subject: [PATCH 21/21] Show the table header along with the message if a query returns no results --- .../app/components/code/TSQLResultsTable.tsx | 65 ++++++++++++++++++- .../app/components/metrics/QueryWidget.tsx | 6 ++ .../app/components/query/QueryEditor.tsx | 1 + 3 files changed, 69 insertions(+), 3 deletions(-) diff --git a/apps/webapp/app/components/code/TSQLResultsTable.tsx b/apps/webapp/app/components/code/TSQLResultsTable.tsx index 62ca631593..36ae3a290c 100644 --- a/apps/webapp/app/components/code/TSQLResultsTable.tsx +++ b/apps/webapp/app/components/code/TSQLResultsTable.tsx @@ -1,7 +1,6 @@ import { ChevronDownIcon, ChevronUpDownIcon, ChevronUpIcon } from "@heroicons/react/20/solid"; import type { OutputColumnMetadata } from "@internal/clickhouse"; import { IconFilter2, IconFilter2X, IconTable } from "@tabler/icons-react"; -import { ChartBlankState } from "../primitives/charts/ChartBlankState"; import { rankItem } from "@tanstack/match-sorter-utils"; import { flexRender, @@ -20,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"; @@ -38,6 +37,8 @@ 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"; @@ -905,11 +906,14 @@ export const TSQLResultsTable = memo(function TSQLResultsTable({ columns, prettyFormatting = true, sorting: defaultSorting = [], + showHeaderOnEmpty = false, }: { rows: Record[]; 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(null); @@ -979,7 +983,62 @@ export const TSQLResultsTable = memo(function TSQLResultsTable({ // Empty state if (rows.length === 0) { - return ; + if (!showHeaderOnEmpty) { + return ; + } + + return ( +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + const meta = header.column.columnDef.meta as ColumnMeta | undefined; + return ( + + ); + })} + + ))} + + + + + + +
+ + {flexRender(header.column.columnDef.header, header.getContext())} + +
+
+ + + This query returned no results + +
+
+
+ ); } return ( diff --git a/apps/webapp/app/components/metrics/QueryWidget.tsx b/apps/webapp/app/components/metrics/QueryWidget.tsx index f403857045..1dcf170399 100644 --- a/apps/webapp/app/components/metrics/QueryWidget.tsx +++ b/apps/webapp/app/components/metrics/QueryWidget.tsx @@ -154,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({ @@ -403,6 +405,7 @@ type QueryWidgetBodyProps = { isFullscreen: boolean; setIsFullscreen: (open: boolean) => void; isLoading: boolean; + showTableHeaderOnEmpty?: boolean; }; function QueryWidgetBody({ @@ -413,6 +416,7 @@ function QueryWidgetBody({ isFullscreen, setIsFullscreen, isLoading, + showTableHeaderOnEmpty, }: QueryWidgetBodyProps) { const type = config.type; @@ -431,6 +435,7 @@ function QueryWidgetBody({ columns={data.columns} prettyFormatting={config.prettyFormatting} sorting={config.sorting} + showHeaderOnEmpty={showTableHeaderOnEmpty} />
diff --git a/apps/webapp/app/components/query/QueryEditor.tsx b/apps/webapp/app/components/query/QueryEditor.tsx index 87f579f3d5..6a986b49f2 100644 --- a/apps/webapp/app/components/query/QueryEditor.tsx +++ b/apps/webapp/app/components/query/QueryEditor.tsx @@ -785,6 +785,7 @@ export function QueryEditor({