Skip to content

Commit 421c625

Browse files
committed
Refactor query page component to streamline imports and remove unused code
This commit simplifies the `page.tsx` file by removing unnecessary imports and unused functions related to custom query handling. It retains essential hooks and types while enhancing code clarity and maintainability. The overall structure is optimized for better readability, setting the stage for future enhancements.
1 parent 5ab0a6f commit 421c625

8 files changed

Lines changed: 612 additions & 466 deletions

File tree

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
"use client";
2+
3+
import { AlignLeft, Loader2, Play } from "lucide-react";
4+
import { useExtracted } from "next-intl";
5+
import { useTheme } from "next-themes";
6+
import { useEffect, useRef, useState } from "react";
7+
import { Light as SyntaxHighlighter } from "react-syntax-highlighter";
8+
import sql from "react-syntax-highlighter/dist/esm/languages/hljs/sql";
9+
import { vs, vs2015 } from "react-syntax-highlighter/dist/esm/styles/hljs";
10+
import { Button } from "../../../../components/ui/button";
11+
import { cn } from "../../../../lib/utils";
12+
13+
SyntaxHighlighter.registerLanguage("sql", sql);
14+
15+
type QueryEditorProps = {
16+
value: string;
17+
disabled: boolean;
18+
isRunning: boolean;
19+
onChange: (value: string) => void;
20+
onFormat: () => void;
21+
onRun: () => void;
22+
};
23+
24+
export function QueryEditor({ value, disabled, isRunning, onChange, onFormat, onRun }: QueryEditorProps) {
25+
const t = useExtracted();
26+
const { resolvedTheme } = useTheme();
27+
const [isDark, setIsDark] = useState(false);
28+
const highlightRef = useRef<HTMLDivElement>(null);
29+
const lineNumberRef = useRef<HTMLDivElement>(null);
30+
const lineCount = Math.max(1, value.split("\n").length);
31+
32+
useEffect(() => {
33+
setIsDark(resolvedTheme === "dark" || document.documentElement.classList.contains("dark"));
34+
}, [resolvedTheme]);
35+
36+
const handleScroll = (event: React.UIEvent<HTMLTextAreaElement>) => {
37+
const { scrollLeft, scrollTop } = event.currentTarget;
38+
if (highlightRef.current) {
39+
highlightRef.current.style.transform = `translate(${-scrollLeft}px, ${-scrollTop}px)`;
40+
}
41+
if (lineNumberRef.current) {
42+
lineNumberRef.current.style.transform = `translateY(${-scrollTop}px)`;
43+
}
44+
};
45+
46+
return (
47+
<div className="flex min-h-[280px] flex-col overflow-hidden rounded-lg border border-neutral-150 bg-white shadow-sm dark:border-neutral-850 dark:bg-neutral-900">
48+
<div className="flex h-10 items-center justify-between border-b border-neutral-150 bg-neutral-50 px-3 dark:border-neutral-800 dark:bg-neutral-950">
49+
<div className="flex items-center gap-2">
50+
<div className="h-2 w-2 rounded-full bg-emerald-500" />
51+
<div className="text-sm font-medium text-neutral-900 dark:text-neutral-100">{t("Query")}</div>
52+
<div className="rounded border border-neutral-200 bg-white px-1.5 py-0.5 text-[11px] font-medium text-neutral-500 dark:border-neutral-800 dark:bg-neutral-900 dark:text-neutral-400">
53+
SQL
54+
</div>
55+
</div>
56+
<div className="flex items-center gap-1.5">
57+
<Button
58+
type="button"
59+
size="smIcon"
60+
variant="ghost"
61+
onClick={onFormat}
62+
disabled={disabled || !value.trim()}
63+
title="Format query"
64+
aria-label="Format query"
65+
>
66+
<AlignLeft className="h-4 w-4" />
67+
</Button>
68+
<Button size="sm" onClick={onRun} disabled={disabled || !value.trim()}>
69+
{isRunning ? <Loader2 className="h-4 w-4 animate-spin" /> : <Play className="h-4 w-4" />}
70+
{t("Run")}
71+
</Button>
72+
</div>
73+
</div>
74+
75+
<div className="grid min-h-0 flex-1 grid-cols-[42px_minmax(0,1fr)] bg-[#fbfcfd] dark:bg-[#090d16]">
76+
<div className="relative overflow-hidden border-r border-neutral-150 bg-neutral-50 dark:border-neutral-800 dark:bg-neutral-950">
77+
<div
78+
ref={lineNumberRef}
79+
className="select-none px-2 py-2.5 text-right font-mono text-[11px] leading-[18px] text-neutral-400 dark:text-neutral-600"
80+
aria-hidden="true"
81+
>
82+
{Array.from({ length: lineCount }, (_, index) => (
83+
<div key={index} className="h-[18px]">
84+
{index + 1}
85+
</div>
86+
))}
87+
</div>
88+
</div>
89+
<div className="relative min-h-[240px] overflow-hidden">
90+
<div className="pointer-events-none absolute inset-0 overflow-hidden">
91+
<div
92+
ref={highlightRef}
93+
className="min-w-full px-3 py-2.5 font-mono text-[12px] leading-[18px]"
94+
style={{ width: "max-content" }}
95+
>
96+
<SyntaxHighlighter
97+
key={isDark ? "sql-dark" : "sql-light"}
98+
language="sql"
99+
style={isDark ? vs2015 : vs}
100+
customStyle={{
101+
margin: 0,
102+
padding: 0,
103+
background: "transparent",
104+
color: isDark ? "#dcdcdc" : "#111827",
105+
fontSize: "12px",
106+
lineHeight: "18px",
107+
overflow: "visible",
108+
whiteSpace: "pre",
109+
}}
110+
codeTagProps={{
111+
style: {
112+
fontFamily: "inherit",
113+
fontSize: "12px",
114+
lineHeight: "18px",
115+
whiteSpace: "pre",
116+
color: isDark ? "#dcdcdc" : "#111827",
117+
},
118+
}}
119+
>
120+
{value || " "}
121+
</SyntaxHighlighter>
122+
</div>
123+
</div>
124+
<textarea
125+
value={value}
126+
onChange={event => onChange(event.target.value)}
127+
onScroll={handleScroll}
128+
disabled={disabled}
129+
spellCheck={false}
130+
wrap="off"
131+
className={cn(
132+
"absolute inset-0 min-h-[240px] resize-none overflow-auto border-0 bg-transparent px-3 py-2.5 font-mono text-[12px] leading-[18px] text-transparent outline-none caret-neutral-900",
133+
"selection:bg-blue-500/20 placeholder:text-neutral-400 focus:ring-0 disabled:cursor-not-allowed disabled:opacity-60",
134+
"dark:caret-neutral-100 dark:selection:bg-blue-400/25 dark:placeholder:text-neutral-600"
135+
)}
136+
/>
137+
</div>
138+
</div>
139+
140+
<div className="flex h-7 items-center justify-between border-t border-neutral-150 bg-neutral-50 px-3 text-[11px] text-neutral-500 dark:border-neutral-800 dark:bg-neutral-950 dark:text-neutral-500">
141+
<span>{lineCount} lines</span>
142+
<span>{value.length.toLocaleString()} chars</span>
143+
</div>
144+
</div>
145+
);
146+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"use client";
2+
3+
import { Loader2, Sparkles, X } from "lucide-react";
4+
import { useExtracted } from "next-intl";
5+
import type { FormEvent } from "react";
6+
import { Button } from "../../../../components/ui/button";
7+
import { Input } from "../../../../components/ui/input";
8+
9+
type QueryPromptFormProps = {
10+
prompt: string;
11+
canUseQuery: boolean;
12+
isBusy: boolean;
13+
isGenerating: boolean;
14+
onPromptChange: (prompt: string) => void;
15+
onGenerate: (event: FormEvent<HTMLFormElement>) => void;
16+
onCancelGenerate: () => void;
17+
};
18+
19+
export function QueryPromptForm({
20+
prompt,
21+
canUseQuery,
22+
isBusy,
23+
isGenerating,
24+
onPromptChange,
25+
onGenerate,
26+
onCancelGenerate,
27+
}: QueryPromptFormProps) {
28+
const t = useExtracted();
29+
30+
return (
31+
<form onSubmit={onGenerate} className="flex flex-col gap-2 md:flex-row">
32+
<Input
33+
value={prompt}
34+
onChange={event => onPromptChange(event.target.value)}
35+
placeholder={t("What do you want to query?")}
36+
disabled={!canUseQuery || isBusy}
37+
className="md:flex-1"
38+
/>
39+
<Button type="submit" disabled={!canUseQuery || !prompt.trim() || isBusy} className="md:w-auto">
40+
{isGenerating ? <Loader2 className="h-4 w-4 animate-spin" /> : <Sparkles className="h-4 w-4" />}
41+
{t("Generate")}
42+
</Button>
43+
{isGenerating && (
44+
<Button type="button" variant="outline" onClick={onCancelGenerate} className="md:w-auto">
45+
<X className="h-4 w-4" />
46+
{t("Cancel")}
47+
</Button>
48+
)}
49+
</form>
50+
);
51+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
"use client";
2+
3+
import { AlertCircle, Loader2, Plus, X } from "lucide-react";
4+
import { Button } from "../../../../components/ui/button";
5+
import { cn } from "../../../../lib/utils";
6+
import type { QueryTab } from "../types";
7+
8+
type QueryTabsProps = {
9+
tabs: QueryTab[];
10+
activeTabId?: string;
11+
runningTabIds: Set<string>;
12+
generatingTabIds: Set<string>;
13+
onSelectTab: (tabId: string) => void;
14+
onCloseTab: (tabId: string) => void;
15+
onAddTab: () => void;
16+
};
17+
18+
export function QueryTabs({
19+
tabs,
20+
activeTabId,
21+
runningTabIds,
22+
generatingTabIds,
23+
onSelectTab,
24+
onCloseTab,
25+
onAddTab,
26+
}: QueryTabsProps) {
27+
return (
28+
<div className="flex items-center gap-1 overflow-x-auto rounded-lg border border-neutral-150 bg-neutral-50 p-1 dark:border-neutral-850 dark:bg-neutral-950">
29+
{tabs.map((tab, index) => {
30+
const isActive = tab.id === activeTabId;
31+
const tabIsBusy = runningTabIds.has(tab.id) || generatingTabIds.has(tab.id);
32+
33+
return (
34+
<button
35+
key={tab.id}
36+
type="button"
37+
onClick={() => onSelectTab(tab.id)}
38+
className={cn(
39+
"group flex h-8 min-w-[120px] items-center justify-between gap-2 rounded-md border px-2 text-left text-xs transition-colors",
40+
isActive
41+
? "border-neutral-200 bg-white text-neutral-900 shadow-sm dark:border-neutral-750 dark:bg-neutral-900 dark:text-neutral-100"
42+
: "border-transparent text-neutral-600 hover:bg-white/70 hover:text-neutral-900 dark:text-neutral-400 dark:hover:bg-neutral-900/70 dark:hover:text-neutral-100"
43+
)}
44+
>
45+
<span className="flex min-w-0 items-center gap-1.5">
46+
{tabIsBusy && <Loader2 className="h-3 w-3 shrink-0 animate-spin text-neutral-400" />}
47+
{tab.resultError && !tabIsBusy && <AlertCircle className="h-3 w-3 shrink-0 text-red-500" />}
48+
<span className="truncate">{tab.name || `Query ${index + 1}`}</span>
49+
</span>
50+
{tabs.length > 1 && (
51+
<span
52+
role="button"
53+
tabIndex={-1}
54+
onClick={event => {
55+
event.stopPropagation();
56+
onCloseTab(tab.id);
57+
}}
58+
className="rounded p-0.5 text-neutral-400 opacity-70 hover:bg-neutral-100 hover:text-neutral-700 group-hover:opacity-100 dark:hover:bg-neutral-800 dark:hover:text-neutral-200"
59+
aria-label="Close query tab"
60+
>
61+
<X className="h-3 w-3" />
62+
</span>
63+
)}
64+
</button>
65+
);
66+
})}
67+
<Button type="button" size="smIcon" variant="ghost" onClick={onAddTab} className="shrink-0" aria-label="New query">
68+
<Plus className="h-4 w-4" />
69+
</Button>
70+
</div>
71+
);
72+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
"use client";
2+
3+
import { AlertCircle } from "lucide-react";
4+
import { useExtracted } from "next-intl";
5+
import type { CustomQueryRow } from "../../../../api/analytics/endpoints";
6+
import type { QueryTab, SortState } from "../types";
7+
import { ResultsTable } from "./ResultsTable";
8+
9+
type ResultsPanelProps = {
10+
activeTab?: QueryTab;
11+
columns: string[];
12+
rows: CustomQueryRow[];
13+
sort: SortState;
14+
onSortChange: (sort: SortState) => void;
15+
};
16+
17+
export function ResultsPanel({ activeTab, columns, rows, sort, onSortChange }: ResultsPanelProps) {
18+
const t = useExtracted();
19+
20+
return (
21+
<div className="flex min-h-0 flex-1 flex-col overflow-hidden rounded-lg border border-neutral-150 bg-white dark:border-neutral-850 dark:bg-neutral-900">
22+
<div className="flex h-10 items-center justify-between border-b border-neutral-100 px-3 dark:border-neutral-850">
23+
<div className="text-sm font-medium text-neutral-900 dark:text-neutral-100">{t("Results")}</div>
24+
<div className="text-xs text-neutral-500 dark:text-neutral-400">
25+
{activeTab?.resultError ? (
26+
<span className="inline-flex items-center gap-1 text-red-600 dark:text-red-400">
27+
<AlertCircle className="h-3.5 w-3.5" />
28+
{t("Error")}
29+
</span>
30+
) : activeTab?.hasRun ? (
31+
t("{count} rows", { count: activeTab.rows.length.toLocaleString() })
32+
) : (
33+
t("Not run")
34+
)}
35+
</div>
36+
</div>
37+
38+
{activeTab?.resultError ? (
39+
<div className="flex min-h-0 flex-1 items-start justify-center overflow-auto p-4">
40+
<div className="w-full rounded-md border border-red-300 bg-red-50 p-4 dark:border-red-800 dark:bg-red-950/40">
41+
<div className="flex items-center gap-2 text-sm font-medium text-red-700 dark:text-red-300">
42+
<AlertCircle className="h-4 w-4" />
43+
{t("Error")}
44+
</div>
45+
<pre className="mt-3 max-h-[320px] overflow-auto whitespace-pre-wrap break-words rounded border border-red-200 bg-white/70 p-3 font-mono text-xs leading-5 text-red-800 dark:border-red-900/70 dark:bg-red-950/50 dark:text-red-100">
46+
{activeTab.resultError}
47+
</pre>
48+
</div>
49+
</div>
50+
) : columns.length > 0 ? (
51+
<ResultsTable columns={columns} rows={rows} sort={sort} onSortChange={onSortChange} />
52+
) : (
53+
<div className="flex min-h-0 flex-1 items-center justify-center text-sm text-neutral-500 dark:text-neutral-400">
54+
{activeTab?.hasRun ? t("No rows returned") : t("Run a query")}
55+
</div>
56+
)}
57+
</div>
58+
);
59+
}

0 commit comments

Comments
 (0)