Better help and added byte seconds stat
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
import { cn } from "~/utils/cn";
|
||||
import { Badge } from "./primitives/Badge";
|
||||
import { SimpleTooltip } from "./primitives/Tooltip";
|
||||
|
||||
export function AlphaBadge({
|
||||
inline = false,
|
||||
className,
|
||||
}: {
|
||||
inline?: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<SimpleTooltip
|
||||
button={
|
||||
<Badge variant="extra-small" className={cn(inline ? "inline-grid" : "", className)}>
|
||||
Alpha
|
||||
</Badge>
|
||||
}
|
||||
content="This feature is in Alpha."
|
||||
disableHoverableContent
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function AlphaTitle({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<>
|
||||
<span>{children}</span>
|
||||
<AlphaBadge />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -97,6 +97,7 @@ import { HelpAndFeedback } from "./HelpAndFeedbackPopover";
|
||||
import { SideMenuHeader } from "./SideMenuHeader";
|
||||
import { SideMenuItem } from "./SideMenuItem";
|
||||
import { SideMenuSection } from "./SideMenuSection";
|
||||
import { AlphaBadge } from "../AlphaBadge";
|
||||
|
||||
type SideMenuUser = Pick<User, "email" | "admin"> & { isImpersonating: boolean };
|
||||
export type SideMenuProject = Pick<
|
||||
@@ -278,6 +279,7 @@ export function SideMenu({
|
||||
activeIconColor="text-purple-500"
|
||||
to={queryPath(organization, project, environment)}
|
||||
data-action="query"
|
||||
badge={<AlphaBadge />}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+56
-38
@@ -1,4 +1,5 @@
|
||||
import { LightBulbIcon } from "@heroicons/react/20/solid";
|
||||
import { ColumnSchema } from "@internal/tsql";
|
||||
import { Form, useNavigation } from "@remix-run/react";
|
||||
import {
|
||||
type ActionFunctionArgs,
|
||||
@@ -9,11 +10,14 @@ import { useState } from "react";
|
||||
import { typedjson, useTypedActionData, useTypedLoaderData } from "remix-typedjson";
|
||||
import { z } from "zod";
|
||||
import { ExitIcon } from "~/assets/icons/ExitIcon";
|
||||
import { AlphaTitle } from "~/components/AlphaBadge";
|
||||
import { TSQLEditor } from "~/components/code/TSQLEditor";
|
||||
import { TSQLResultsTable } from "~/components/code/TSQLResultsTable";
|
||||
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
|
||||
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
|
||||
import { Badge } from "~/components/primitives/Badge";
|
||||
import { Button } from "~/components/primitives/Buttons";
|
||||
import { CopyableText } from "~/components/primitives/CopyableText";
|
||||
import { Header2, Header3 } from "~/components/primitives/Headers";
|
||||
import { NavBar, PageTitle } from "~/components/primitives/PageHeader";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
@@ -194,7 +198,7 @@ export default function Page() {
|
||||
return (
|
||||
<PageContainer>
|
||||
<NavBar>
|
||||
<PageTitle title="Query" />
|
||||
<PageTitle title={<AlphaTitle>Query</AlphaTitle>} />
|
||||
</NavBar>
|
||||
<PageBody scrollable={false}>
|
||||
<ResizablePanelGroup orientation="horizontal" className="max-h-full">
|
||||
@@ -211,6 +215,13 @@ export default function Page() {
|
||||
showClearButton={true}
|
||||
minHeight="200px"
|
||||
className="min-h-[200px]"
|
||||
additionalActions={
|
||||
showHelpSidebar ? null : (
|
||||
<Button variant="minimal/small" onClick={() => setShowHelpSidebar(true)}>
|
||||
Help
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Form method="post" className="flex items-center justify-end gap-2 px-2">
|
||||
<input type="hidden" name="query" value={query} />
|
||||
@@ -345,43 +356,9 @@ function QueryHelpSidebar({ onClose }: { onClose: () => void }) {
|
||||
</Paragraph>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-2 divide-y divide-grid-dimmed">
|
||||
{Object.values(table.columns).map((col) => (
|
||||
<div
|
||||
key={col.name}
|
||||
className="rounded border border-grid-dimmed bg-charcoal-900 p-2"
|
||||
>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<code className="text-sm font-medium text-text-bright">{col.name}</code>
|
||||
<span className="text-xs text-text-dimmed">{col.type}</span>
|
||||
</div>
|
||||
{col.description && (
|
||||
<Paragraph variant="extra-small" className="mt-1 text-text-dimmed">
|
||||
{col.description}
|
||||
</Paragraph>
|
||||
)}
|
||||
{col.example && (
|
||||
<div className="mt-1.5 flex items-baseline gap-1.5">
|
||||
<span className="text-xs text-text-dimmed">Example:</span>
|
||||
<code className="text-xs text-green-400">{col.example}</code>
|
||||
</div>
|
||||
)}
|
||||
{col.allowedValues && col.allowedValues.length > 0 && (
|
||||
<div className="mt-1.5">
|
||||
<span className="text-xs text-text-dimmed">Allowed values:</span>
|
||||
<div className="mt-1 flex flex-wrap gap-1">
|
||||
{col.allowedValues.map((value) => (
|
||||
<code
|
||||
key={value}
|
||||
className="rounded bg-charcoal-800 px-1.5 py-0.5 text-xs text-amber-400"
|
||||
>
|
||||
{col.valueMap?.[value] ?? value}
|
||||
</code>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<ColumnHelpItem key={col.name} col={col} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -391,6 +368,43 @@ function QueryHelpSidebar({ onClose }: { onClose: () => void }) {
|
||||
);
|
||||
}
|
||||
|
||||
function ColumnHelpItem({ col }: { col: ColumnSchema }) {
|
||||
return (
|
||||
<div className="pt-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<CopyableText value={col.name} className="text-sm text-indigo-400" />
|
||||
<Badge className="font-mono text-xxs">{col.type}</Badge>
|
||||
</div>
|
||||
{col.description && (
|
||||
<Paragraph variant="extra-small" className="mt-1 text-text-dimmed">
|
||||
{col.description}
|
||||
</Paragraph>
|
||||
)}
|
||||
{col.example && (
|
||||
<div className="mt-1 flex items-baseline gap-0.5">
|
||||
<span className="text-xs text-text-dimmed">Example:</span>
|
||||
<CopyableText
|
||||
value={col.example}
|
||||
className="rounded-sm bg-charcoal-750 px-1.5 py-0.5 font-mono text-xxs"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{col.allowedValues && col.allowedValues.length > 0 && (
|
||||
<div className="mt-0.5 flex flex-wrap gap-1">
|
||||
<span className="text-xs text-text-dimmed">Available options:</span>
|
||||
{col.allowedValues.map((value) => (
|
||||
<CopyableText
|
||||
key={value}
|
||||
value={col.valueMap?.[value] ?? value}
|
||||
className="rounded-sm bg-charcoal-750 px-1.5 py-0.5 font-mono text-xxs"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ScopeItem({ scope }: { scope: QueryScope }) {
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
@@ -412,17 +426,21 @@ function formatQueryStats(stats: {
|
||||
read_rows: string;
|
||||
read_bytes: string;
|
||||
elapsed_ns: string;
|
||||
byte_seconds: string;
|
||||
}): string {
|
||||
const readRows = parseInt(stats.read_rows, 10);
|
||||
const readBytes = parseInt(stats.read_bytes, 10);
|
||||
const elapsedNs = parseInt(stats.elapsed_ns, 10);
|
||||
const byteSeconds = parseFloat(stats.byte_seconds);
|
||||
|
||||
const elapsedMs = elapsedNs / 1_000_000;
|
||||
const formattedTime =
|
||||
elapsedMs < 1000 ? `${elapsedMs.toFixed(1)}ms` : `${(elapsedMs / 1000).toFixed(2)}s`;
|
||||
const formattedBytes = formatBytes(readBytes);
|
||||
|
||||
return `${readRows.toLocaleString()} rows read · ${formattedBytes} · ${formattedTime}`;
|
||||
return `${readRows.toLocaleString()} rows read · ${formattedBytes} · ${formattedTime} · ${formatBytes(
|
||||
byteSeconds
|
||||
)}s`;
|
||||
}
|
||||
|
||||
function formatBytes(bytes: number): string {
|
||||
|
||||
@@ -137,6 +137,11 @@ export const runsSchema: TableSchema = {
|
||||
name: "idempotency_key",
|
||||
...column("String", { description: "Idempotency key", example: "user-123-action-456" }),
|
||||
},
|
||||
region: {
|
||||
name: "region",
|
||||
clickhouseName: "region",
|
||||
...column("String", { description: "Region", example: "us-east-1" }),
|
||||
},
|
||||
|
||||
// Timing
|
||||
created_at: {
|
||||
|
||||
@@ -354,10 +354,16 @@ export class ClickhouseClient implements ClickhouseReader, ClickhouseWriter {
|
||||
result_rows: "0",
|
||||
result_bytes: "0",
|
||||
elapsed_ns: "0",
|
||||
byte_seconds: "0",
|
||||
};
|
||||
|
||||
if (typeof summaryHeader === "string") {
|
||||
const parsedSummary = JSON.parse(summaryHeader);
|
||||
this.logger.log("parsedSummary", parsedSummary);
|
||||
const readBytes = parsedSummary.read_bytes ? parseInt(parsedSummary.read_bytes, 10) : 0;
|
||||
const elapsedNs = parsedSummary.elapsed_ns ? parseInt(parsedSummary.elapsed_ns, 10) : 0;
|
||||
const elapsedSeconds = elapsedNs / 1_000_000_000;
|
||||
const byteSeconds = readBytes / elapsedSeconds;
|
||||
stats = {
|
||||
read_rows: parsedSummary.read_rows ?? "0",
|
||||
read_bytes: parsedSummary.read_bytes ?? "0",
|
||||
@@ -367,6 +373,7 @@ export class ClickhouseClient implements ClickhouseReader, ClickhouseWriter {
|
||||
result_rows: parsedSummary.result_rows ?? "0",
|
||||
result_bytes: parsedSummary.result_bytes ?? "0",
|
||||
elapsed_ns: parsedSummary.elapsed_ns ?? "0",
|
||||
byte_seconds: byteSeconds.toString(),
|
||||
};
|
||||
span.setAttributes({
|
||||
...flattenAttributes(parsedSummary, "clickhouse.summary"),
|
||||
|
||||
@@ -77,6 +77,7 @@ export class NoopClient implements ClickhouseReader, ClickhouseWriter {
|
||||
result_rows: "0",
|
||||
result_bytes: "0",
|
||||
elapsed_ns: "0",
|
||||
byte_seconds: "0",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -25,6 +25,7 @@ export interface QueryStats {
|
||||
result_rows: string;
|
||||
result_bytes: string;
|
||||
elapsed_ns: string;
|
||||
byte_seconds: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user