49df40cb11
TRQL (pronounced Treacle like the delicious British dark sweet syrup) is the TRiggerQueryLanguage. It allows users to safely write queries on their data. The queries are safely turned into ClickHouse queries which are tenant-safe and not SQL injectable. https://github.com/user-attachments/assets/bbfca473-b3fc-4150-8fe6-79e8840a2d29 This started out as a translation of HogQL by PostHog from Python to TypeScript. Features - Tenant safe queries. - Many underlying ClickHouse features including functions and aggregations. - Virtual columns, which are exposed to users as real columns but are actually expressions. - Transformations of data types and where clauses. - Simple JSON path querying. - Limits on execution time. - Reporting of query statistics. ## Query page There’s a new Query page (currently behind a feature flag) where you can write TRQL queries and execute them against your environment, project or organization. Features - Executing TRQL queries - Syntax highlighting and errors - Autocomplete - AI generation/editing of queries - Help and examples - Table with auto-inferred data types from the table schema - Table cell renderers for our special types like Run ids, environments, machines, tasks, queues, etc. - Copy/export as CSV/JSON - Line and bar graphs with grouping and stacking - History of queries
471 lines
15 KiB
TypeScript
471 lines
15 KiB
TypeScript
import { ChevronRightIcon } from "@heroicons/react/24/solid";
|
|
import { Link } from "@remix-run/react";
|
|
import { ClipboardCheckIcon, ClipboardIcon } from "lucide-react";
|
|
import React, { type ReactNode, createContext, forwardRef, useContext, useState } from "react";
|
|
import { useCopy } from "~/hooks/useCopy";
|
|
import { cn } from "~/utils/cn";
|
|
import { Popover, PopoverContent, PopoverVerticalEllipseTrigger } from "./Popover";
|
|
import { InfoIconTooltip, SimpleTooltip } from "./Tooltip";
|
|
|
|
const variants = {
|
|
bright: {
|
|
header: "bg-background-bright",
|
|
cell: "group-hover/table-row:bg-charcoal-750 group-has-[[tabindex='0']:focus]/table-row:bg-charcoal-750",
|
|
stickyCell: "bg-background-bright group-hover/table-row:bg-charcoal-750",
|
|
menuButton:
|
|
"bg-background-bright group-hover/table-row:bg-charcoal-750 group-hover/table-row:ring-charcoal-600/70 group-has-[[tabindex='0']:focus]/table-row:bg-charcoal-750",
|
|
menuButtonDivider: "group-hover/table-row:border-charcoal-600/70",
|
|
rowSelected: "bg-charcoal-750 group-hover/table-row:bg-charcoal-750",
|
|
},
|
|
"bright/no-hover": {
|
|
header: "bg-transparent",
|
|
cell: "group-hover/table-row:bg-transparent",
|
|
stickyCell: "bg-background-bright",
|
|
menuButton: "bg-background-bright",
|
|
menuButtonDivider: "",
|
|
rowSelected: "bg-charcoal-750",
|
|
},
|
|
dimmed: {
|
|
header: "bg-background-dimmed",
|
|
cell: "group-hover/table-row:bg-charcoal-800 group-has-[[tabindex='0']:focus]/table-row:bg-background-bright",
|
|
stickyCell: "group-hover/table-row:bg-charcoal-800",
|
|
menuButton:
|
|
"bg-background-dimmed group-hover/table-row:bg-charcoal-800 group-hover/table-row:ring-grid-bright group-has-[[tabindex='0']:focus]/table-row:bg-background-bright",
|
|
menuButtonDivider: "group-hover/table-row:border-grid-bright",
|
|
rowSelected: "bg-charcoal-750 group-hover/table-row:bg-charcoal-750",
|
|
},
|
|
} as const;
|
|
|
|
export type TableVariant = keyof typeof variants;
|
|
|
|
type TableProps = {
|
|
containerClassName?: string;
|
|
className?: string;
|
|
children: ReactNode;
|
|
fullWidth?: boolean;
|
|
};
|
|
|
|
// Add TableContext
|
|
const TableContext = createContext<{ variant: TableVariant }>({ variant: "dimmed" });
|
|
|
|
export const Table = forwardRef<HTMLTableElement, TableProps & { variant?: TableVariant }>(
|
|
({ className, containerClassName, children, fullWidth, variant = "dimmed" }, ref) => {
|
|
return (
|
|
<TableContext.Provider value={{ variant }}>
|
|
<div
|
|
className={cn(
|
|
"overflow-x-auto whitespace-nowrap border-t scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600",
|
|
containerClassName,
|
|
fullWidth && "w-full"
|
|
)}
|
|
>
|
|
<table ref={ref} className={cn("w-full", className)}>
|
|
{children}
|
|
</table>
|
|
</div>
|
|
</TableContext.Provider>
|
|
);
|
|
}
|
|
);
|
|
|
|
type TableHeaderProps = {
|
|
className?: string;
|
|
children: ReactNode;
|
|
};
|
|
|
|
export const TableHeader = forwardRef<HTMLTableSectionElement, TableHeaderProps>(
|
|
({ className, children }, ref) => {
|
|
const { variant } = useContext(TableContext);
|
|
return (
|
|
<thead
|
|
ref={ref}
|
|
className={cn(
|
|
"sticky top-0 z-10 after:absolute after:bottom-0 after:left-0 after:right-0 after:h-px after:bg-grid-bright",
|
|
variants[variant].header,
|
|
className
|
|
)}
|
|
>
|
|
{children}
|
|
</thead>
|
|
);
|
|
}
|
|
);
|
|
|
|
type TableBodyProps = {
|
|
className?: string;
|
|
children?: ReactNode;
|
|
};
|
|
|
|
export const TableBody = forwardRef<HTMLTableSectionElement, TableBodyProps>(
|
|
({ className, children }, ref) => {
|
|
return (
|
|
<tbody ref={ref} className={cn("relative overflow-y-auto", className)}>
|
|
{children}
|
|
</tbody>
|
|
);
|
|
}
|
|
);
|
|
|
|
type TableRowProps = JSX.IntrinsicElements["tr"] & {
|
|
className?: string;
|
|
children: ReactNode;
|
|
disabled?: boolean;
|
|
isSelected?: boolean;
|
|
};
|
|
|
|
export const TableRow = forwardRef<HTMLTableRowElement, TableRowProps>(
|
|
({ className, disabled, isSelected, children, ...props }, ref) => {
|
|
const { variant } = useContext(TableContext);
|
|
return (
|
|
<tr
|
|
ref={ref}
|
|
{...props}
|
|
className={cn(
|
|
"group/table-row relative w-full outline-none after:absolute after:bottom-0 after:left-3 after:right-0 after:h-px after:bg-grid-dimmed",
|
|
isSelected && variants[variant].rowSelected,
|
|
disabled && "opacity-50",
|
|
className
|
|
)}
|
|
>
|
|
{children}
|
|
</tr>
|
|
);
|
|
}
|
|
);
|
|
|
|
type TableCellBasicProps = {
|
|
className?: string;
|
|
alignment?: "left" | "center" | "right";
|
|
children?: ReactNode;
|
|
colSpan?: number;
|
|
};
|
|
|
|
type TableHeaderCellProps = TableCellBasicProps & {
|
|
hiddenLabel?: boolean;
|
|
tooltip?: ReactNode;
|
|
};
|
|
|
|
export const TableHeaderCell = forwardRef<HTMLTableCellElement, TableHeaderCellProps>(
|
|
({ className, alignment = "left", children, colSpan, hiddenLabel = false, tooltip }, ref) => {
|
|
let alignmentClassName = "text-left";
|
|
switch (alignment) {
|
|
case "center":
|
|
alignmentClassName = "text-center";
|
|
break;
|
|
case "right":
|
|
alignmentClassName = "text-right";
|
|
break;
|
|
}
|
|
|
|
const [isHovered, setIsHovered] = useState(false);
|
|
|
|
return (
|
|
<th
|
|
ref={ref}
|
|
scope="col"
|
|
className={cn(
|
|
"px-3 py-2.5 pb-3 align-middle text-sm font-medium text-text-bright",
|
|
alignmentClassName,
|
|
className
|
|
)}
|
|
colSpan={colSpan}
|
|
tabIndex={-1}
|
|
onMouseEnter={() => setIsHovered(true)}
|
|
onMouseLeave={() => setIsHovered(false)}
|
|
>
|
|
{hiddenLabel ? (
|
|
<span className="sr-only">{children}</span>
|
|
) : tooltip ? (
|
|
<div
|
|
className={cn("flex items-center gap-1", {
|
|
"justify-center": alignment === "center",
|
|
"justify-end": alignment === "right",
|
|
})}
|
|
>
|
|
{children}
|
|
<InfoIconTooltip
|
|
content={tooltip}
|
|
contentClassName="normal-case tracking-normal"
|
|
enabled={isHovered}
|
|
/>
|
|
</div>
|
|
) : (
|
|
children
|
|
)}
|
|
</th>
|
|
);
|
|
}
|
|
);
|
|
|
|
type TableCellProps = TableCellBasicProps & {
|
|
to?: string;
|
|
onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
|
hasAction?: boolean;
|
|
isSticky?: boolean;
|
|
actionClassName?: string;
|
|
rowHoverStyle?: string;
|
|
isSelected?: boolean;
|
|
isTabbableCell?: boolean;
|
|
children?: ReactNode;
|
|
};
|
|
|
|
export const TableCell = forwardRef<HTMLTableCellElement, TableCellProps>(
|
|
(
|
|
{
|
|
className,
|
|
actionClassName,
|
|
alignment = "left",
|
|
children,
|
|
colSpan,
|
|
to,
|
|
onClick,
|
|
hasAction = false,
|
|
isSticky = false,
|
|
isSelected,
|
|
isTabbableCell = false,
|
|
},
|
|
ref
|
|
) => {
|
|
let alignmentClassName = "text-left";
|
|
switch (alignment) {
|
|
case "center":
|
|
alignmentClassName = "text-center";
|
|
break;
|
|
case "right":
|
|
alignmentClassName = "text-right";
|
|
break;
|
|
}
|
|
|
|
const flexClasses = cn(
|
|
"flex w-full whitespace-nowrap px-3 py-3 items-center text-xs text-text-dimmed",
|
|
alignment === "left"
|
|
? "justify-start text-left"
|
|
: alignment === "center"
|
|
? "justify-center text-center"
|
|
: "justify-end text-right"
|
|
);
|
|
const { variant } = useContext(TableContext);
|
|
|
|
return (
|
|
<td
|
|
ref={ref}
|
|
className={cn(
|
|
"text-xs text-charcoal-400 has-[[tabindex='0']:focus]:before:absolute has-[[tabindex='0']:focus]:before:-top-px has-[[tabindex='0']:focus]:before:left-0 has-[[tabindex='0']:focus]:before:h-px has-[[tabindex='0']:focus]:before:w-3 has-[[tabindex='0']:focus]:before:bg-grid-dimmed has-[[tabindex='0']:focus]:after:absolute has-[[tabindex='0']:focus]:after:bottom-0 has-[[tabindex='0']:focus]:after:left-0 has-[[tabindex='0']:focus]:after:right-0 has-[[tabindex='0']:focus]:after:h-px has-[[tabindex='0']:focus]:after:bg-grid-dimmed",
|
|
variants[variant].cell,
|
|
to || onClick || hasAction ? "cursor-pointer" : "cursor-default px-3 py-3 align-middle",
|
|
!to && !onClick && alignmentClassName,
|
|
isSticky &&
|
|
"[&:has(.group-hover/table-row:block)]:w-auto sticky right-0 bg-background-dimmed",
|
|
isSticky && variants[variant].stickyCell,
|
|
isSelected && variants[variant].rowSelected,
|
|
!isSelected &&
|
|
"group-hover/table-row:before:absolute group-hover/table-row:before:left-0 group-hover/table-row:before:top-[-1px] group-hover/table-row:before:h-px group-hover/table-row:before:w-3 group-hover/table-row:before:bg-charcoal-750 group-hover/table-row:after:absolute group-hover/table-row:after:bottom-0 group-hover/table-row:after:left-0 group-hover/table-row:after:h-px group-hover/table-row:after:w-3 group-hover/table-row:after:bg-charcoal-750 group-focus-visible/table-row:bg-background-bright",
|
|
className
|
|
)}
|
|
colSpan={colSpan}
|
|
>
|
|
{to ? (
|
|
<Link
|
|
to={to}
|
|
className={cn("cursor-pointer focus:outline-none", flexClasses, actionClassName)}
|
|
tabIndex={isTabbableCell ? 0 : -1}
|
|
>
|
|
{children}
|
|
</Link>
|
|
) : onClick ? (
|
|
<button
|
|
onClick={onClick}
|
|
className={cn("cursor-pointer focus:outline-none", flexClasses, actionClassName)}
|
|
tabIndex={isTabbableCell ? 0 : -1}
|
|
>
|
|
{children}
|
|
</button>
|
|
) : (
|
|
<>{children}</>
|
|
)}
|
|
</td>
|
|
);
|
|
}
|
|
);
|
|
|
|
type CopyableTableCellProps = TableCellProps & {
|
|
value: string;
|
|
};
|
|
|
|
export const CopyableTableCell = forwardRef<HTMLTableCellElement, CopyableTableCellProps>(
|
|
({ value, children, className, ...props }, ref) => {
|
|
const [isHovered, setIsHovered] = useState(false);
|
|
const { copy, copied } = useCopy(value);
|
|
|
|
return (
|
|
<TableCell ref={ref} className={className} {...props}>
|
|
<div
|
|
className="relative flex items-center"
|
|
onMouseEnter={() => setIsHovered(true)}
|
|
onMouseLeave={() => setIsHovered(false)}
|
|
>
|
|
{children}
|
|
{isHovered && (
|
|
<span
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
copy();
|
|
}}
|
|
className="absolute -right-2 top-1/2 z-10 flex -translate-y-1/2 cursor-pointer"
|
|
>
|
|
<SimpleTooltip
|
|
button={
|
|
<span
|
|
className={cn(
|
|
"flex size-6 items-center justify-center rounded border border-charcoal-650 bg-charcoal-750",
|
|
copied
|
|
? "text-green-500"
|
|
: "text-text-dimmed hover:border-charcoal-600 hover:bg-charcoal-700 hover:text-text-bright"
|
|
)}
|
|
>
|
|
{copied ? (
|
|
<ClipboardCheckIcon className="size-3.5" />
|
|
) : (
|
|
<ClipboardIcon className="size-3.5" />
|
|
)}
|
|
</span>
|
|
}
|
|
content={copied ? "Copied!" : "Copy"}
|
|
disableHoverableContent
|
|
/>
|
|
</span>
|
|
)}
|
|
</div>
|
|
</TableCell>
|
|
);
|
|
}
|
|
);
|
|
|
|
export const TableCellChevron = forwardRef<
|
|
HTMLTableCellElement,
|
|
{
|
|
className?: string;
|
|
to?: string;
|
|
children?: ReactNode;
|
|
isSticky?: boolean;
|
|
onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
|
}
|
|
>(({ className, to, children, isSticky, onClick }, ref) => {
|
|
return (
|
|
<TableCell
|
|
className={className}
|
|
isSticky={isSticky}
|
|
to={to}
|
|
onClick={onClick}
|
|
ref={ref}
|
|
alignment="right"
|
|
>
|
|
{children}
|
|
<ChevronRightIcon className="size-4 text-text-dimmed transition group-hover:text-text-bright" />
|
|
</TableCell>
|
|
);
|
|
});
|
|
|
|
export const TableCellMenu = forwardRef<
|
|
HTMLTableCellElement,
|
|
TableCellProps & {
|
|
className?: string;
|
|
isSticky?: boolean;
|
|
onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
|
visibleButtons?: ReactNode;
|
|
hiddenButtons?: ReactNode;
|
|
popoverContent?: ReactNode;
|
|
children?: ReactNode;
|
|
isSelected?: boolean;
|
|
}
|
|
>(
|
|
(
|
|
{
|
|
className,
|
|
isSticky,
|
|
onClick,
|
|
visibleButtons,
|
|
hiddenButtons,
|
|
popoverContent,
|
|
children,
|
|
isSelected,
|
|
},
|
|
ref
|
|
) => {
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const { variant } = useContext(TableContext);
|
|
|
|
return (
|
|
<TableCell
|
|
className={className}
|
|
isSticky={isSticky}
|
|
onClick={onClick}
|
|
ref={ref}
|
|
alignment="right"
|
|
hasAction={true}
|
|
isSelected={isSelected}
|
|
>
|
|
<div className="relative h-full p-1">
|
|
<div
|
|
className={cn(
|
|
"absolute right-0 top-1/2 mr-1 flex -translate-y-1/2 items-center justify-end gap-0.5 rounded-[0.25rem] p-0.5 group-hover/table-row:ring-1",
|
|
variants[variant].menuButton
|
|
)}
|
|
>
|
|
{/* Hidden buttons that show on hover */}
|
|
{hiddenButtons && (
|
|
<div
|
|
className={cn(
|
|
"hidden group-hover/table-row:block",
|
|
popoverContent && "pr-0.5 group-hover/table-row:border-r",
|
|
variants[variant].menuButtonDivider
|
|
)}
|
|
>
|
|
<div className={cn("flex items-center gap-x-0.5 divide-x divide-grid-bright")}>
|
|
{hiddenButtons}
|
|
</div>
|
|
</div>
|
|
)}
|
|
{/* Always visible buttons */}
|
|
{visibleButtons}
|
|
{/* Always visible popover with ellipsis trigger */}
|
|
{popoverContent && (
|
|
<Popover onOpenChange={(open) => setIsOpen(open)}>
|
|
<PopoverVerticalEllipseTrigger
|
|
isOpen={isOpen}
|
|
className="duration-0 group-hover/table-row:text-text-bright"
|
|
/>
|
|
<PopoverContent
|
|
className="min-w-[10rem] max-w-[20rem] overflow-y-auto p-0 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600"
|
|
align="end"
|
|
>
|
|
<div className="flex flex-col gap-1 p-1">{popoverContent}</div>
|
|
</PopoverContent>
|
|
</Popover>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</TableCell>
|
|
);
|
|
}
|
|
);
|
|
|
|
type TableBlankRowProps = {
|
|
className?: string;
|
|
colSpan: number;
|
|
children?: ReactNode;
|
|
};
|
|
|
|
export const TableBlankRow = forwardRef<HTMLTableRowElement, TableBlankRowProps>(
|
|
({ children, colSpan, className }, ref) => {
|
|
return (
|
|
<tr ref={ref}>
|
|
<td colSpan={colSpan} className={cn("py-6 text-center text-sm", className)}>
|
|
{children}
|
|
</td>
|
|
</tr>
|
|
);
|
|
}
|
|
);
|