Skip to content

Commit e4fb4d0

Browse files
committed
Separated the Query stuff into more files as it was… long
1 parent 9d10769 commit e4fb4d0

8 files changed

Lines changed: 1672 additions & 1639 deletions

File tree

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { useState } from "react";
2+
import { AIQueryInput } from "~/components/code/AIQueryInput";
3+
import { Header3 } from "~/components/primitives/Headers";
4+
5+
export function AITabContent({
6+
onQueryGenerated,
7+
getCurrentQuery,
8+
aiFixRequest,
9+
}: {
10+
onQueryGenerated: (query: string) => void;
11+
getCurrentQuery: () => string;
12+
aiFixRequest: { prompt: string; key: number } | null;
13+
}) {
14+
const [examplePromptRequest, setExamplePromptRequest] = useState<{
15+
prompt: string;
16+
key: number;
17+
} | null>(null);
18+
19+
// Use aiFixRequest if present, otherwise use example prompt request
20+
const activeRequest = aiFixRequest ?? examplePromptRequest;
21+
22+
const examplePrompts = [
23+
"Show me failed runs by hour for the past 7 days",
24+
"Count of runs by status by hour for the past 48h",
25+
"Top 50 most expensive runs this week",
26+
"Average execution duration by task this week",
27+
"Run counts by tag in the past 7 days",
28+
];
29+
30+
return (
31+
<div className="space-y-2">
32+
<AIQueryInput
33+
onQueryGenerated={onQueryGenerated}
34+
autoSubmitPrompt={activeRequest?.prompt}
35+
autoSubmitKey={activeRequest?.key}
36+
getCurrentQuery={getCurrentQuery}
37+
/>
38+
39+
<div className="pt-4">
40+
<Header3 className="mb-2 text-text-bright">Example prompts</Header3>
41+
<div className="space-y-2">
42+
{examplePrompts.map((example) => (
43+
<button
44+
key={example}
45+
type="button"
46+
onClick={() => {
47+
setExamplePromptRequest((prev) => ({
48+
prompt: example,
49+
key: (prev?.key ?? 0) + 1,
50+
}));
51+
}}
52+
className="block w-full rounded-md border border-grid-dimmed bg-charcoal-800 px-3 py-2 text-left text-sm text-text-dimmed transition-colors hover:border-grid-bright hover:bg-charcoal-750 hover:text-text-bright"
53+
>
54+
{example}
55+
</button>
56+
))}
57+
</div>
58+
</div>
59+
</div>
60+
);
61+
}
62+
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { Header3 } from "~/components/primitives/Headers";
2+
import { Paragraph } from "~/components/primitives/Paragraph";
3+
import type { QueryScope } from "~/services/queryService.server";
4+
import { TryableCodeBlock } from "./TRQLGuideContent";
5+
6+
// Example queries for the Examples tab
7+
export const exampleQueries: Array<{
8+
title: string;
9+
description: string;
10+
query: string;
11+
scope: QueryScope;
12+
}> = [
13+
{
14+
title: "Failed runs by task (past 7 days)",
15+
description: "Count of failed runs grouped by task identifier over the last 7 days.",
16+
query: `SELECT
17+
task_identifier,
18+
count() AS failed_count
19+
FROM runs
20+
WHERE status = 'Failed'
21+
AND triggered_at > now() - INTERVAL 7 DAY
22+
GROUP BY task_identifier
23+
ORDER BY failed_count DESC
24+
LIMIT 20`,
25+
scope: "environment",
26+
},
27+
{
28+
title: "Execution duration p50 by task (past 7d)",
29+
description: "Median (50th percentile) execution duration for each task.",
30+
query: `SELECT
31+
task_identifier,
32+
quantile(0.5)(execution_duration) AS p50_duration_ms
33+
FROM runs
34+
WHERE triggered_at > now() - INTERVAL 7 DAY
35+
AND execution_duration IS NOT NULL
36+
GROUP BY task_identifier
37+
ORDER BY p50_duration_ms DESC
38+
LIMIT 20`,
39+
scope: "environment",
40+
},
41+
{
42+
title: "Most expensive 100 runs (past 7d)",
43+
description: "Top 100 runs by cost over the last 7 days.",
44+
query: `SELECT
45+
run_id,
46+
task_identifier,
47+
status,
48+
total_cost,
49+
usage_duration,
50+
machine,
51+
created_at
52+
FROM runs
53+
WHERE triggered_at > now() - INTERVAL 7 DAY
54+
ORDER BY total_cost DESC
55+
LIMIT 100`,
56+
scope: "environment",
57+
},
58+
];
59+
60+
export function ExamplesContent({
61+
onTryExample,
62+
}: {
63+
onTryExample: (query: string, scope: QueryScope) => void;
64+
}) {
65+
return (
66+
<div className="space-y-6">
67+
{exampleQueries.map((example) => (
68+
<div key={example.title}>
69+
<Header3 className="mb-1 text-text-bright">{example.title}</Header3>
70+
<Paragraph variant="small" className="mb-2 text-text-dimmed">
71+
{example.description}
72+
</Paragraph>
73+
<TryableCodeBlock
74+
code={example.query}
75+
onTry={() => onTryExample(example.query, example.scope)}
76+
/>
77+
</div>
78+
))}
79+
</div>
80+
);
81+
}
82+
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { AISparkleIcon } from "~/assets/icons/AISparkleIcon";
2+
import {
3+
ClientTabs,
4+
ClientTabsContent,
5+
ClientTabsList,
6+
ClientTabsTrigger,
7+
} from "~/components/primitives/ClientTabs";
8+
import type { QueryScope } from "~/services/queryService.server";
9+
import { AITabContent } from "./AITabContent";
10+
import { ExamplesContent } from "./ExamplesContent";
11+
import { TableSchemaContent } from "./TableSchemaContent";
12+
import { TRQLGuideContent } from "./TRQLGuideContent";
13+
14+
export function QueryHelpSidebar({
15+
onTryExample,
16+
onQueryGenerated,
17+
getCurrentQuery,
18+
activeTab,
19+
onTabChange,
20+
aiFixRequest,
21+
}: {
22+
onTryExample: (query: string, scope: QueryScope) => void;
23+
onQueryGenerated: (query: string) => void;
24+
getCurrentQuery: () => string;
25+
activeTab: string;
26+
onTabChange: (tab: string) => void;
27+
aiFixRequest: { prompt: string; key: number } | null;
28+
}) {
29+
return (
30+
<div className="grid h-full max-h-full grid-rows-[auto_1fr] overflow-hidden bg-background-bright">
31+
<ClientTabs
32+
value={activeTab}
33+
onValueChange={onTabChange}
34+
className="flex min-h-0 flex-col overflow-hidden pt-1"
35+
>
36+
<ClientTabsList variant="underline" className="mx-3 shrink-0">
37+
<ClientTabsTrigger value="ai" variant="underline" layoutId="query-help-tabs">
38+
<div className="flex items-center gap-0.5">
39+
<AISparkleIcon className="size-4" /> AI
40+
</div>
41+
</ClientTabsTrigger>
42+
<ClientTabsTrigger value="guide" variant="underline" layoutId="query-help-tabs">
43+
Writing TRQL
44+
</ClientTabsTrigger>
45+
<ClientTabsTrigger value="schema" variant="underline" layoutId="query-help-tabs">
46+
Table schema
47+
</ClientTabsTrigger>
48+
<ClientTabsTrigger value="examples" variant="underline" layoutId="query-help-tabs">
49+
Examples
50+
</ClientTabsTrigger>
51+
</ClientTabsList>
52+
<ClientTabsContent
53+
value="ai"
54+
className="min-h-0 flex-1 overflow-y-auto p-3 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600"
55+
>
56+
<AITabContent
57+
onQueryGenerated={onQueryGenerated}
58+
getCurrentQuery={getCurrentQuery}
59+
aiFixRequest={aiFixRequest}
60+
/>
61+
</ClientTabsContent>
62+
<ClientTabsContent
63+
value="guide"
64+
className="min-h-0 flex-1 overflow-y-auto p-3 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600"
65+
>
66+
<TRQLGuideContent onTryExample={onTryExample} />
67+
</ClientTabsContent>
68+
<ClientTabsContent
69+
value="schema"
70+
className="min-h-0 flex-1 overflow-y-auto p-3 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600"
71+
>
72+
<TableSchemaContent />
73+
</ClientTabsContent>
74+
<ClientTabsContent
75+
value="examples"
76+
className="min-h-0 flex-1 overflow-y-auto p-3 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600"
77+
>
78+
<ExamplesContent onTryExample={onTryExample} />
79+
</ClientTabsContent>
80+
</ClientTabs>
81+
</div>
82+
);
83+
}
84+
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { useState } from "react";
2+
import { ClockRotateLeftIcon } from "~/assets/icons/ClockRotateLeftIcon";
3+
import { Button } from "~/components/primitives/Buttons";
4+
import { DateTime } from "~/components/primitives/DateTime";
5+
import { Popover, PopoverContent, PopoverTrigger } from "~/components/primitives/Popover";
6+
import type { QueryHistoryItem } from "~/presenters/v3/QueryPresenter.server";
7+
8+
const SQL_KEYWORDS = [
9+
"SELECT",
10+
"FROM",
11+
"WHERE",
12+
"ORDER BY",
13+
"LIMIT",
14+
"GROUP BY",
15+
"HAVING",
16+
"JOIN",
17+
"LEFT JOIN",
18+
"RIGHT JOIN",
19+
"INNER JOIN",
20+
"OUTER JOIN",
21+
"AND",
22+
"OR",
23+
"AS",
24+
"ON",
25+
"IN",
26+
"NOT",
27+
"NULL",
28+
"DESC",
29+
"ASC",
30+
"DISTINCT",
31+
"COUNT",
32+
"SUM",
33+
"AVG",
34+
"MIN",
35+
"MAX",
36+
];
37+
38+
function highlightSQL(query: string): React.ReactNode[] {
39+
// Normalize whitespace for display (let CSS line-clamp handle truncation)
40+
const normalized = query.replace(/\s+/g, " ").slice(0, 200);
41+
const suffix = "";
42+
43+
// Create a regex pattern that matches keywords as whole words (case insensitive)
44+
const keywordPattern = new RegExp(
45+
`\\b(${SQL_KEYWORDS.map((k) => k.replace(/\s+/g, "\\s+")).join("|")})\\b`,
46+
"gi"
47+
);
48+
49+
const parts: React.ReactNode[] = [];
50+
let lastIndex = 0;
51+
let match;
52+
53+
while ((match = keywordPattern.exec(normalized)) !== null) {
54+
// Add text before the match
55+
if (match.index > lastIndex) {
56+
parts.push(normalized.slice(lastIndex, match.index));
57+
}
58+
// Add the highlighted keyword
59+
parts.push(
60+
<span key={match.index} className="text-[#c678dd]">
61+
{match[0]}
62+
</span>
63+
);
64+
lastIndex = keywordPattern.lastIndex;
65+
}
66+
67+
// Add remaining text
68+
if (lastIndex < normalized.length) {
69+
parts.push(normalized.slice(lastIndex));
70+
}
71+
72+
if (suffix) {
73+
parts.push(suffix);
74+
}
75+
76+
return parts;
77+
}
78+
79+
export function QueryHistoryPopover({
80+
history,
81+
onQuerySelected,
82+
}: {
83+
history: QueryHistoryItem[];
84+
onQuerySelected: (item: QueryHistoryItem) => void;
85+
}) {
86+
const [isOpen, setIsOpen] = useState(false);
87+
88+
return (
89+
<Popover open={isOpen} onOpenChange={setIsOpen}>
90+
<PopoverTrigger asChild>
91+
<Button
92+
type="button"
93+
variant="tertiary/small"
94+
LeadingIcon={ClockRotateLeftIcon}
95+
disabled={history.length === 0}
96+
>
97+
History
98+
</Button>
99+
</PopoverTrigger>
100+
<PopoverContent
101+
className="w-[400px] min-w-0 overflow-hidden p-0"
102+
align="start"
103+
sideOffset={6}
104+
>
105+
<div className="max-h-80 overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
106+
<div className="p-1">
107+
{history.map((item) => (
108+
<button
109+
key={item.id}
110+
type="button"
111+
onClick={() => {
112+
onQuerySelected(item);
113+
setIsOpen(false);
114+
}}
115+
className="flex w-full items-center gap-2 rounded-sm px-2 py-2 outline-none transition-colors focus-custom hover:bg-charcoal-900"
116+
>
117+
<div className="flex flex-1 flex-col items-start overflow-hidden">
118+
<p className="line-clamp-2 w-full break-words text-left font-mono text-xs text-[#9b99ff]">
119+
{highlightSQL(item.query)}
120+
</p>
121+
<div className="flex items-center gap-2 text-xs text-text-dimmed">
122+
<DateTime date={item.createdAt} showTooltip={false} />
123+
{item.userName && <span>· {item.userName}</span>}
124+
<span className="capitalize">· {item.scope}</span>
125+
</div>
126+
</div>
127+
</button>
128+
))}
129+
</div>
130+
</div>
131+
</PopoverContent>
132+
</Popover>
133+
);
134+
}
135+

0 commit comments

Comments
 (0)