lytx 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +37 -0
- package/README.md +486 -0
- package/alchemy.run.ts +155 -0
- package/cli/bootstrap-admin.ts +284 -0
- package/cli/deploy-staging.ts +692 -0
- package/cli/import-events.ts +628 -0
- package/cli/import-sites.ts +518 -0
- package/cli/index.ts +609 -0
- package/cli/init-db.ts +269 -0
- package/cli/migrate-to-durable-objects.ts +564 -0
- package/cli/migration-worker.ts +300 -0
- package/cli/performance-test.ts +588 -0
- package/cli/pg/client.ts +4 -0
- package/cli/pg/new-site.ts +153 -0
- package/cli/rollback-durable-objects.ts +622 -0
- package/cli/seed-data.ts +459 -0
- package/cli/setup.js +18 -0
- package/cli/setup.ts +463 -0
- package/cli/validate-migration.ts +200 -0
- package/cli/wrangler-migration.jsonc +28 -0
- package/db/adapter.ts +166 -0
- package/db/analytics_engine/client.ts +0 -0
- package/db/analytics_engine/sites.ts +0 -0
- package/db/client.ts +16 -0
- package/db/d1/client.ts +8 -0
- package/db/d1/drizzle.config.ts +35 -0
- package/db/d1/migrations/0000_true_maelstrom.sql +165 -0
- package/db/d1/migrations/0001_wonderful_bloodaxe.sql +12 -0
- package/db/d1/migrations/0002_late_frightful_four.sql +1 -0
- package/db/d1/migrations/0003_cuddly_obadiah_stane.sql +16 -0
- package/db/d1/migrations/0004_mute_stardust.sql +1 -0
- package/db/d1/migrations/0005_awesome_silvermane.sql +3 -0
- package/db/d1/migrations/0006_volatile_shriek.sql +2 -0
- package/db/d1/migrations/0007_superb_lila_cheney.sql +1 -0
- package/db/d1/migrations/0008_bitter_longshot.sql +17 -0
- package/db/d1/migrations/0009_wonderful_madame_masque.sql +28 -0
- package/db/d1/migrations/meta/0000_snapshot.json +1112 -0
- package/db/d1/migrations/meta/0001_snapshot.json +1187 -0
- package/db/d1/migrations/meta/0002_snapshot.json +1194 -0
- package/db/d1/migrations/meta/0003_snapshot.json +1296 -0
- package/db/d1/migrations/meta/0004_snapshot.json +1303 -0
- package/db/d1/migrations/meta/0005_snapshot.json +1325 -0
- package/db/d1/migrations/meta/0006_snapshot.json +1339 -0
- package/db/d1/migrations/meta/0007_snapshot.json +1347 -0
- package/db/d1/migrations/meta/0008_snapshot.json +1464 -0
- package/db/d1/migrations/meta/0009_snapshot.json +1648 -0
- package/db/d1/migrations/meta/_journal.json +76 -0
- package/db/d1/schema.ts +407 -0
- package/db/d1/sites.ts +374 -0
- package/db/d1/teamAiUsage.ts +101 -0
- package/db/d1/teams.ts +127 -0
- package/db/durable/drizzle.config.ts +8 -0
- package/db/durable/durableObjectClient.ts +480 -0
- package/db/durable/events.ts +100 -0
- package/db/durable/migrations/0000_fair_bucky.sql +38 -0
- package/db/durable/migrations/meta/0000_snapshot.json +278 -0
- package/db/durable/migrations/meta/_journal.json +13 -0
- package/db/durable/migrations/migrations.js +10 -0
- package/db/durable/schema.ts +5 -0
- package/db/durable/siteDurableObject.ts +1352 -0
- package/db/durable/types.ts +53 -0
- package/db/postgres/client.ts +13 -0
- package/db/postgres/drizzle.config.ts +12 -0
- package/db/postgres/migrations/0000_brainy_sprite.sql +116 -0
- package/db/postgres/migrations/meta/0000_snapshot.json +681 -0
- package/db/postgres/migrations/meta/_journal.json +13 -0
- package/db/postgres/schema.ts +145 -0
- package/db/postgres/sites.ts +118 -0
- package/db/tranformReports.ts +595 -0
- package/db/types.ts +55 -0
- package/endpoints/api_worker.tsx +1854 -0
- package/endpoints/site_do_worker.ts +11 -0
- package/index.d.ts +63 -0
- package/index.ts +83 -0
- package/lib/auth.ts +279 -0
- package/lib/geojson/world_countries.json +45307 -0
- package/lib/random_name.ts +41 -0
- package/lib/sendMail.ts +252 -0
- package/package.json +142 -0
- package/public/favicon.ico +0 -0
- package/public/images/android-chrome-192x192.png +0 -0
- package/public/images/android-chrome-512x512.png +0 -0
- package/public/images/apple-touch-icon.png +0 -0
- package/public/images/favicon-16x16.png +0 -0
- package/public/images/favicon-32x32.png +0 -0
- package/public/images/lytx_dark_dashboard.png +0 -0
- package/public/images/lytx_light_dashboard.png +0 -0
- package/public/images/safari-pinned-tab.svg +4 -0
- package/public/logo.png +0 -0
- package/public/site.webmanifest +26 -0
- package/public/sw.js +107 -0
- package/src/Document.tsx +86 -0
- package/src/api/ai_api.ts +1156 -0
- package/src/api/authMiddleware.ts +45 -0
- package/src/api/auth_api.ts +465 -0
- package/src/api/event_labels_api.ts +193 -0
- package/src/api/events_api.ts +210 -0
- package/src/api/queueWorker.ts +303 -0
- package/src/api/reports_api.ts +278 -0
- package/src/api/seed_api.ts +288 -0
- package/src/api/sites_api.ts +904 -0
- package/src/api/tag_api.ts +458 -0
- package/src/api/tag_api_v2.ts +289 -0
- package/src/api/team_api.ts +456 -0
- package/src/app/Dashboard.tsx +1339 -0
- package/src/app/Events.tsx +974 -0
- package/src/app/Explore.tsx +312 -0
- package/src/app/Layout.tsx +58 -0
- package/src/app/Settings.tsx +1302 -0
- package/src/app/components/DashboardCard.tsx +118 -0
- package/src/app/components/EditableCell.tsx +123 -0
- package/src/app/components/EventForm.tsx +93 -0
- package/src/app/components/MarketingFooter.tsx +49 -0
- package/src/app/components/MarketingNav.tsx +150 -0
- package/src/app/components/Nav.tsx +755 -0
- package/src/app/components/NewSiteSetup.tsx +298 -0
- package/src/app/components/SQLEditor.tsx +740 -0
- package/src/app/components/SiteSelector.tsx +126 -0
- package/src/app/components/SiteTag.tsx +42 -0
- package/src/app/components/SiteTagInstallCard.tsx +241 -0
- package/src/app/components/WorldMapCard.tsx +337 -0
- package/src/app/components/charts/ChartComponents.tsx +1481 -0
- package/src/app/components/charts/EventFunnel.tsx +45 -0
- package/src/app/components/charts/EventSummary.tsx +194 -0
- package/src/app/components/charts/SankeyFlows.tsx +72 -0
- package/src/app/components/marketing/CheckIcon.tsx +16 -0
- package/src/app/components/marketing/MarketingLayout.tsx +23 -0
- package/src/app/components/marketing/SectionHeading.tsx +35 -0
- package/src/app/components/reports/AskAiWorkspace.tsx +371 -0
- package/src/app/components/reports/CreateReportStarter.tsx +74 -0
- package/src/app/components/reports/DashboardRouteFiltersContext.tsx +14 -0
- package/src/app/components/reports/DashboardToolbar.tsx +154 -0
- package/src/app/components/reports/DashboardWorkspaceLayout.tsx +63 -0
- package/src/app/components/reports/DashboardWorkspaceShell.tsx +118 -0
- package/src/app/components/reports/ReportBuilderWorkspace.tsx +76 -0
- package/src/app/components/reports/custom/CustomReportBuilderPage.tsx +1667 -0
- package/src/app/components/reports/custom/ReportWidgetChart.tsx +297 -0
- package/src/app/components/reports/custom/buildWidgetSql.ts +151 -0
- package/src/app/components/reports/custom/chartPalettes.ts +18 -0
- package/src/app/components/reports/custom/types.ts +50 -0
- package/src/app/components/reports/reportBuilderMenuItems.ts +17 -0
- package/src/app/components/reports/useDashboardToolbarControls.tsx +235 -0
- package/src/app/components/ui/AlertBanner.tsx +101 -0
- package/src/app/components/ui/Button.tsx +55 -0
- package/src/app/components/ui/Card.tsx +80 -0
- package/src/app/components/ui/Input.tsx +72 -0
- package/src/app/components/ui/Link.tsx +23 -0
- package/src/app/components/ui/ReportBuilderMenu.tsx +246 -0
- package/src/app/components/ui/ThemeToggle.tsx +54 -0
- package/src/app/constants.ts +6 -0
- package/src/app/headers.ts +33 -0
- package/src/app/providers/AuthProvider.tsx +189 -0
- package/src/app/providers/ClientProviders.tsx +18 -0
- package/src/app/providers/QueryProvider.tsx +23 -0
- package/src/app/providers/ThemeProvider.tsx +88 -0
- package/src/app/utils/chartThemes.ts +146 -0
- package/src/app/utils/keybinds.ts +96 -0
- package/src/app/utils/media.tsx +24 -0
- package/src/client.tsx +114 -0
- package/src/config/createLytxAppConfig.ts +252 -0
- package/src/config/resourceNames.ts +88 -0
- package/src/db/index.ts +67 -0
- package/src/index.css +285 -0
- package/src/lib/featureFlags.ts +69 -0
- package/src/pages/GetStarted.tsx +290 -0
- package/src/pages/Home.tsx +268 -0
- package/src/pages/Login.tsx +283 -0
- package/src/pages/PrivacyPolicy.tsx +120 -0
- package/src/pages/Signup.tsx +267 -0
- package/src/pages/TermsOfService.tsx +126 -0
- package/src/pages/VerifyEmail.tsx +56 -0
- package/src/session/durableObject.ts +7 -0
- package/src/session/siteSchema.ts +86 -0
- package/src/session/types.ts +36 -0
- package/src/templates/README.md +80 -0
- package/src/templates/cleanFunctions.js +44 -0
- package/src/templates/embedFunctions.js +52 -0
- package/src/templates/lytx-shared.ts +662 -0
- package/src/templates/lytxpixel-core.ts +144 -0
- package/src/templates/lytxpixel.ts +267 -0
- package/src/templates/lytxpixelBrowser.js +634 -0
- package/src/templates/lytxpixelBrowser.mjs +634 -0
- package/src/templates/parseData.js +12 -0
- package/src/templates/script.ts +31 -0
- package/src/templates/template.tsx +50 -0
- package/src/templates/test.js +3 -0
- package/src/templates/trackWebEvents.ts +177 -0
- package/src/templates/vendors/clickcease.ts +8 -0
- package/src/templates/vendors/google.ts +174 -0
- package/src/templates/vendors/linkedin.ts +23 -0
- package/src/templates/vendors/meta.ts +56 -0
- package/src/templates/vendors/quantcast.ts +22 -0
- package/src/templates/vendors/simplfi.ts +7 -0
- package/src/types/app-context.ts +16 -0
- package/src/utilities/dashboardParams.ts +188 -0
- package/src/utilities/dashboardQueries.ts +537 -0
- package/src/utilities/dashboardTransforms.ts +167 -0
- package/src/utilities/dataValidation.ts +414 -0
- package/src/utilities/detector.ts +73 -0
- package/src/utilities/encrypt.ts +103 -0
- package/src/utilities/index.ts +13 -0
- package/src/utilities/parser.ts +117 -0
- package/src/utilities/performanceMonitoring.ts +570 -0
- package/src/utilities/route_interuptors.ts +24 -0
- package/src/worker.tsx +675 -0
- package/tsconfig.json +78 -0
- package/types/env.d.ts +16 -0
- package/types/rw.d.ts +7 -0
- package/types/shims.d.ts +53 -0
- package/types/vite.d.ts +19 -0
- package/vite/vite-plugin-pixel-bundle.ts +126 -0
- package/vite.config.ts +53 -0
- package/worker-configuration.d.ts +8401 -0
|
@@ -0,0 +1,740 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
import { init } from "modern-monaco";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Example queries to help users get started.
|
|
8
|
+
* These are static since they're documentation, not schema-derived.
|
|
9
|
+
*/
|
|
10
|
+
const EXAMPLE_QUERIES = [
|
|
11
|
+
{
|
|
12
|
+
name: "Recent Events",
|
|
13
|
+
query: `SELECT event, page_url, created_at
|
|
14
|
+
FROM site_events
|
|
15
|
+
ORDER BY created_at DESC
|
|
16
|
+
LIMIT 50`,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: "Events by Type",
|
|
20
|
+
query: `SELECT event, COUNT(*) as count
|
|
21
|
+
FROM site_events
|
|
22
|
+
GROUP BY event
|
|
23
|
+
ORDER BY count DESC`,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: "Find Unnamed Events",
|
|
27
|
+
query: `SELECT event, COUNT(*) as count,
|
|
28
|
+
datetime(MIN(created_at), 'unixepoch') as first_seen_utc,
|
|
29
|
+
datetime(MAX(created_at), 'unixepoch') as last_seen_utc
|
|
30
|
+
FROM site_events
|
|
31
|
+
WHERE lower(event) LIKE '%unnamed%'
|
|
32
|
+
GROUP BY event
|
|
33
|
+
ORDER BY count DESC
|
|
34
|
+
LIMIT 100`,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: "Top Pages",
|
|
38
|
+
query: `SELECT page_url, COUNT(*) as views
|
|
39
|
+
FROM site_events
|
|
40
|
+
WHERE event = 'pageview'
|
|
41
|
+
GROUP BY page_url
|
|
42
|
+
ORDER BY views DESC
|
|
43
|
+
LIMIT 20`,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: "Traffic by Country",
|
|
47
|
+
query: `SELECT country, COUNT(*) as visits
|
|
48
|
+
FROM site_events
|
|
49
|
+
WHERE country IS NOT NULL
|
|
50
|
+
GROUP BY country
|
|
51
|
+
ORDER BY visits DESC
|
|
52
|
+
LIMIT 10`,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: "Device Breakdown",
|
|
56
|
+
query: `SELECT device_type, COUNT(*) as count
|
|
57
|
+
FROM site_events
|
|
58
|
+
WHERE device_type IS NOT NULL
|
|
59
|
+
GROUP BY device_type`,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: "Browser Usage",
|
|
63
|
+
query: `SELECT browser, COUNT(*) as count
|
|
64
|
+
FROM site_events
|
|
65
|
+
WHERE browser IS NOT NULL
|
|
66
|
+
GROUP BY browser
|
|
67
|
+
ORDER BY count DESC`,
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
/** SQL keywords for autocomplete */
|
|
72
|
+
const SQL_KEYWORDS = [
|
|
73
|
+
"SELECT", "FROM", "WHERE", "AND", "OR", "NOT", "IN", "LIKE", "BETWEEN",
|
|
74
|
+
"IS", "NULL", "AS", "ORDER", "BY", "ASC", "DESC", "LIMIT", "OFFSET",
|
|
75
|
+
"GROUP", "HAVING", "DISTINCT", "COUNT", "SUM", "AVG", "MIN", "MAX",
|
|
76
|
+
"JOIN", "LEFT", "RIGHT", "INNER", "OUTER", "ON", "UNION", "ALL",
|
|
77
|
+
"CASE", "WHEN", "THEN", "ELSE", "END", "CAST", "COALESCE", "IFNULL",
|
|
78
|
+
"STRFTIME", "DATE", "TIME", "DATETIME", "EXISTS", "WITH"
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
/** Column info returned from PRAGMA table_info */
|
|
82
|
+
interface ColumnInfo {
|
|
83
|
+
name: string;
|
|
84
|
+
type: string;
|
|
85
|
+
nullable: boolean;
|
|
86
|
+
primaryKey: boolean;
|
|
87
|
+
defaultValue: string | null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Index info returned from PRAGMA index_list + index_info */
|
|
91
|
+
interface IndexInfo {
|
|
92
|
+
name: string;
|
|
93
|
+
columns: string[];
|
|
94
|
+
unique: boolean;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Table schema returned from the API */
|
|
98
|
+
interface TableSchema {
|
|
99
|
+
name: string;
|
|
100
|
+
columns: ColumnInfo[];
|
|
101
|
+
indexes: IndexInfo[];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** API response shape */
|
|
105
|
+
interface SchemaResponse {
|
|
106
|
+
tables: TableSchema[];
|
|
107
|
+
siteId: number | null;
|
|
108
|
+
error?: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface SQLEditorProps {
|
|
112
|
+
/** The SQL query value */
|
|
113
|
+
value: string;
|
|
114
|
+
/** Callback when the query changes */
|
|
115
|
+
onChange?: (value: string) => void;
|
|
116
|
+
/** Callback when user presses Ctrl+Enter to submit */
|
|
117
|
+
onSubmit?: () => void;
|
|
118
|
+
/** Placeholder text when editor is empty */
|
|
119
|
+
placeholder?: string;
|
|
120
|
+
/** Whether the editor is disabled/read-only */
|
|
121
|
+
disabled?: boolean;
|
|
122
|
+
/** Height of the editor (default: "200px") */
|
|
123
|
+
height?: string;
|
|
124
|
+
/** Monaco theme (default: "github-dark") */
|
|
125
|
+
theme?: string;
|
|
126
|
+
/** Additional class names for the container */
|
|
127
|
+
className?: string;
|
|
128
|
+
/** Whether to show schema tab (default: true) */
|
|
129
|
+
showSchema?: boolean;
|
|
130
|
+
/** Site ID to fetch schema for */
|
|
131
|
+
siteId?: number | null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Schema viewer component showing table structure and example queries.
|
|
136
|
+
* Fetches schema from the durable object at runtime.
|
|
137
|
+
*/
|
|
138
|
+
function SchemaViewer({
|
|
139
|
+
onSelectQuery,
|
|
140
|
+
height,
|
|
141
|
+
siteId,
|
|
142
|
+
}: {
|
|
143
|
+
onSelectQuery?: (query: string) => void;
|
|
144
|
+
height: string;
|
|
145
|
+
siteId?: number | null;
|
|
146
|
+
}) {
|
|
147
|
+
const [expandedSection, setExpandedSection] = useState<"columns" | "indexes" | "examples">("columns");
|
|
148
|
+
const [schema, setSchema] = useState<TableSchema | null>(null);
|
|
149
|
+
const [loading, setLoading] = useState(false);
|
|
150
|
+
const [error, setError] = useState<string | null>(null);
|
|
151
|
+
|
|
152
|
+
useEffect(() => {
|
|
153
|
+
if (!siteId) {
|
|
154
|
+
setError("No site selected");
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const fetchSchema = async () => {
|
|
159
|
+
setLoading(true);
|
|
160
|
+
setError(null);
|
|
161
|
+
try {
|
|
162
|
+
const response = await fetch(`/api/site-events/schema?site_id=${siteId}`);
|
|
163
|
+
const data = await response.json() as SchemaResponse;
|
|
164
|
+
|
|
165
|
+
if (!response.ok) {
|
|
166
|
+
setError(data.error || "Failed to fetch schema");
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (data.tables && data.tables.length > 0) {
|
|
171
|
+
setSchema(data.tables[0]);
|
|
172
|
+
} else {
|
|
173
|
+
setError("No tables found");
|
|
174
|
+
}
|
|
175
|
+
} catch (err) {
|
|
176
|
+
setError("Failed to fetch schema");
|
|
177
|
+
console.error("Schema fetch error:", err);
|
|
178
|
+
} finally {
|
|
179
|
+
setLoading(false);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
fetchSchema();
|
|
184
|
+
}, [siteId]);
|
|
185
|
+
|
|
186
|
+
if (loading) {
|
|
187
|
+
return (
|
|
188
|
+
<div
|
|
189
|
+
className="flex items-center justify-center bg-[var(--theme-input-bg)] rounded-lg border border-[var(--theme-input-border)]"
|
|
190
|
+
style={{ height, minHeight: "120px" }}
|
|
191
|
+
>
|
|
192
|
+
<div className="text-sm text-[var(--theme-text-secondary)]">Loading schema...</div>
|
|
193
|
+
</div>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (error || !schema) {
|
|
198
|
+
return (
|
|
199
|
+
<div
|
|
200
|
+
className="overflow-y-auto bg-[var(--theme-input-bg)] rounded-lg border border-[var(--theme-input-border)]"
|
|
201
|
+
style={{ height, minHeight: "120px" }}
|
|
202
|
+
>
|
|
203
|
+
{/* Show error but still allow access to examples */}
|
|
204
|
+
<div className="p-3 border-b border-[var(--theme-input-border)]">
|
|
205
|
+
{error && (
|
|
206
|
+
<div className="text-xs text-amber-400 mb-2">
|
|
207
|
+
{error}
|
|
208
|
+
</div>
|
|
209
|
+
)}
|
|
210
|
+
<div className="flex items-center gap-2">
|
|
211
|
+
<span className="text-xs font-mono px-2 py-1 bg-blue-500/20 text-blue-400 rounded">
|
|
212
|
+
TABLE
|
|
213
|
+
</span>
|
|
214
|
+
<code className="text-sm font-semibold text-[var(--theme-text-primary)]">
|
|
215
|
+
site_events
|
|
216
|
+
</code>
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
|
|
220
|
+
{/* Still show example queries */}
|
|
221
|
+
<div className="p-3 space-y-3">
|
|
222
|
+
<p className="text-xs text-[var(--theme-text-secondary)] mb-2">
|
|
223
|
+
Example queries:
|
|
224
|
+
</p>
|
|
225
|
+
{EXAMPLE_QUERIES.map((example) => (
|
|
226
|
+
<div key={example.name} className="space-y-1">
|
|
227
|
+
<div className="flex items-center justify-between">
|
|
228
|
+
<span className="text-xs font-medium text-[var(--theme-text-primary)]">
|
|
229
|
+
{example.name}
|
|
230
|
+
</span>
|
|
231
|
+
{onSelectQuery && (
|
|
232
|
+
<button
|
|
233
|
+
onClick={() => onSelectQuery(example.query)}
|
|
234
|
+
className="text-[10px] px-2 py-1 rounded bg-blue-500/20 text-blue-400 hover:bg-blue-500/30 transition-colors"
|
|
235
|
+
>
|
|
236
|
+
Use Query
|
|
237
|
+
</button>
|
|
238
|
+
)}
|
|
239
|
+
</div>
|
|
240
|
+
<pre className="text-[11px] font-mono p-2 rounded bg-[var(--theme-bg-secondary)] text-[var(--theme-text-secondary)] overflow-x-auto whitespace-pre-wrap">
|
|
241
|
+
{example.query}
|
|
242
|
+
</pre>
|
|
243
|
+
</div>
|
|
244
|
+
))}
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return (
|
|
251
|
+
<div
|
|
252
|
+
className="overflow-y-auto bg-[var(--theme-input-bg)] rounded-lg border border-[var(--theme-input-border)]"
|
|
253
|
+
style={{ height, minHeight: "120px" }}
|
|
254
|
+
>
|
|
255
|
+
<div className="p-3 border-b border-[var(--theme-input-border)]">
|
|
256
|
+
<div className="flex items-center gap-2">
|
|
257
|
+
<span className="text-xs font-mono px-2 py-1 bg-blue-500/20 text-blue-400 rounded">
|
|
258
|
+
TABLE
|
|
259
|
+
</span>
|
|
260
|
+
<code className="text-sm font-semibold text-[var(--theme-text-primary)]">
|
|
261
|
+
{schema.name}
|
|
262
|
+
</code>
|
|
263
|
+
</div>
|
|
264
|
+
<p className="text-xs text-[var(--theme-text-secondary)] mt-1">
|
|
265
|
+
Stores all tracked events for a site
|
|
266
|
+
</p>
|
|
267
|
+
</div>
|
|
268
|
+
|
|
269
|
+
{/* Section Tabs */}
|
|
270
|
+
<div className="flex border-b border-[var(--theme-input-border)]">
|
|
271
|
+
{(["columns", "indexes", "examples"] as const).map((section) => (
|
|
272
|
+
<button
|
|
273
|
+
key={section}
|
|
274
|
+
onClick={() => setExpandedSection(section)}
|
|
275
|
+
className={`flex-1 px-3 py-2 text-xs font-medium transition-colors ${
|
|
276
|
+
expandedSection === section
|
|
277
|
+
? "bg-[var(--theme-bg-secondary)] text-[var(--theme-text-primary)] border-b-2 border-blue-500"
|
|
278
|
+
: "text-[var(--theme-text-secondary)] hover:text-[var(--theme-text-primary)]"
|
|
279
|
+
}`}
|
|
280
|
+
>
|
|
281
|
+
{section === "columns" && `Columns (${schema.columns.length})`}
|
|
282
|
+
{section === "indexes" && `Indexes (${schema.indexes.length})`}
|
|
283
|
+
{section === "examples" && `Examples (${EXAMPLE_QUERIES.length})`}
|
|
284
|
+
</button>
|
|
285
|
+
))}
|
|
286
|
+
</div>
|
|
287
|
+
|
|
288
|
+
{/* Columns Section */}
|
|
289
|
+
{expandedSection === "columns" && (
|
|
290
|
+
<div className="divide-y divide-[var(--theme-input-border)]">
|
|
291
|
+
{schema.columns.map((col) => (
|
|
292
|
+
<div key={col.name} className="px-3 py-2 hover:bg-[var(--theme-bg-secondary)]">
|
|
293
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
294
|
+
<code className="text-xs font-mono text-[var(--theme-text-primary)]">
|
|
295
|
+
{col.name}
|
|
296
|
+
</code>
|
|
297
|
+
<span className="text-[10px] px-1.5 py-0.5 rounded bg-[var(--theme-bg-secondary)] text-[var(--theme-text-secondary)]">
|
|
298
|
+
{col.type || "TEXT"}
|
|
299
|
+
</span>
|
|
300
|
+
{col.primaryKey && (
|
|
301
|
+
<span className="text-[10px] px-1.5 py-0.5 rounded bg-blue-500/20 text-blue-400">
|
|
302
|
+
PRIMARY KEY
|
|
303
|
+
</span>
|
|
304
|
+
)}
|
|
305
|
+
{!col.nullable && !col.primaryKey && (
|
|
306
|
+
<span className="text-[10px] px-1.5 py-0.5 rounded bg-amber-500/20 text-amber-400">
|
|
307
|
+
NOT NULL
|
|
308
|
+
</span>
|
|
309
|
+
)}
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
))}
|
|
313
|
+
</div>
|
|
314
|
+
)}
|
|
315
|
+
|
|
316
|
+
{/* Indexes Section */}
|
|
317
|
+
{expandedSection === "indexes" && (
|
|
318
|
+
<div className="p-3 space-y-2">
|
|
319
|
+
<p className="text-xs text-[var(--theme-text-secondary)] mb-2">
|
|
320
|
+
Indexed columns for optimized queries:
|
|
321
|
+
</p>
|
|
322
|
+
{schema.indexes.map((idx) => (
|
|
323
|
+
<div
|
|
324
|
+
key={idx.name}
|
|
325
|
+
className="px-2 py-1.5 bg-[var(--theme-bg-secondary)] rounded text-xs font-mono text-[var(--theme-text-secondary)]"
|
|
326
|
+
>
|
|
327
|
+
<span className="text-[var(--theme-text-primary)]">{idx.name}</span>
|
|
328
|
+
{" "}({idx.columns.join(", ")})
|
|
329
|
+
{idx.unique && (
|
|
330
|
+
<span className="ml-2 text-[10px] px-1.5 py-0.5 rounded bg-green-500/20 text-green-400">
|
|
331
|
+
UNIQUE
|
|
332
|
+
</span>
|
|
333
|
+
)}
|
|
334
|
+
</div>
|
|
335
|
+
))}
|
|
336
|
+
</div>
|
|
337
|
+
)}
|
|
338
|
+
|
|
339
|
+
{/* Example Queries Section */}
|
|
340
|
+
{expandedSection === "examples" && (
|
|
341
|
+
<div className="p-3 space-y-3">
|
|
342
|
+
{EXAMPLE_QUERIES.map((example) => (
|
|
343
|
+
<div key={example.name} className="space-y-1">
|
|
344
|
+
<div className="flex items-center justify-between">
|
|
345
|
+
<span className="text-xs font-medium text-[var(--theme-text-primary)]">
|
|
346
|
+
{example.name}
|
|
347
|
+
</span>
|
|
348
|
+
{onSelectQuery && (
|
|
349
|
+
<button
|
|
350
|
+
onClick={() => onSelectQuery(example.query)}
|
|
351
|
+
className="text-[10px] px-2 py-1 rounded bg-blue-500/20 text-blue-400 hover:bg-blue-500/30 transition-colors"
|
|
352
|
+
>
|
|
353
|
+
Use Query
|
|
354
|
+
</button>
|
|
355
|
+
)}
|
|
356
|
+
</div>
|
|
357
|
+
<pre className="text-[11px] font-mono p-2 rounded bg-[var(--theme-bg-secondary)] text-[var(--theme-text-secondary)] overflow-x-auto whitespace-pre-wrap">
|
|
358
|
+
{example.query}
|
|
359
|
+
</pre>
|
|
360
|
+
</div>
|
|
361
|
+
))}
|
|
362
|
+
</div>
|
|
363
|
+
)}
|
|
364
|
+
</div>
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* SQL Editor component using Monaco editor with SQL/SQLite syntax highlighting.
|
|
370
|
+
* Includes a schema tab that fetches table structure from the durable object at runtime.
|
|
371
|
+
* Provides autocomplete for table names, column names, and SQL keywords.
|
|
372
|
+
*
|
|
373
|
+
* Autocomplete: Press Ctrl+Space (or Cmd+Space on Mac) to trigger suggestions.
|
|
374
|
+
*
|
|
375
|
+
* Reusable across different parts of the application.
|
|
376
|
+
*/
|
|
377
|
+
export function SQLEditor({
|
|
378
|
+
value,
|
|
379
|
+
onChange,
|
|
380
|
+
onSubmit,
|
|
381
|
+
placeholder = "SELECT * FROM site_events LIMIT 10",
|
|
382
|
+
disabled = false,
|
|
383
|
+
height = "200px",
|
|
384
|
+
theme = "github-dark",
|
|
385
|
+
className = "",
|
|
386
|
+
showSchema = true,
|
|
387
|
+
siteId,
|
|
388
|
+
}: SQLEditorProps) {
|
|
389
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
390
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
391
|
+
const editorRef = useRef<any>(null);
|
|
392
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
393
|
+
const monacoRef = useRef<any>(null);
|
|
394
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
395
|
+
const completionProviderRef = useRef<any>(null);
|
|
396
|
+
const isInitialized = useRef(false);
|
|
397
|
+
const [activeTab, setActiveTab] = useState<"editor" | "schema">("editor");
|
|
398
|
+
const [schema, setSchema] = useState<TableSchema | null>(null);
|
|
399
|
+
// Keep schema in a ref so completion provider can access latest value
|
|
400
|
+
const schemaRef = useRef<TableSchema | null>(null);
|
|
401
|
+
schemaRef.current = schema;
|
|
402
|
+
|
|
403
|
+
// Stable callbacks
|
|
404
|
+
const onChangeRef = useRef(onChange);
|
|
405
|
+
onChangeRef.current = onChange;
|
|
406
|
+
const onSubmitRef = useRef(onSubmit);
|
|
407
|
+
onSubmitRef.current = onSubmit;
|
|
408
|
+
|
|
409
|
+
// Fetch schema for autocomplete
|
|
410
|
+
useEffect(() => {
|
|
411
|
+
if (!siteId) return;
|
|
412
|
+
|
|
413
|
+
const fetchSchema = async () => {
|
|
414
|
+
try {
|
|
415
|
+
const response = await fetch(`/api/site-events/schema?site_id=${siteId}`);
|
|
416
|
+
const data = await response.json() as SchemaResponse;
|
|
417
|
+
|
|
418
|
+
if (response.ok && data.tables && data.tables.length > 0) {
|
|
419
|
+
setSchema(data.tables[0]);
|
|
420
|
+
}
|
|
421
|
+
} catch (err) {
|
|
422
|
+
console.error("Failed to fetch schema for autocomplete:", err);
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
fetchSchema();
|
|
427
|
+
}, [siteId]);
|
|
428
|
+
|
|
429
|
+
useEffect(() => {
|
|
430
|
+
if (!containerRef.current || isInitialized.current) return;
|
|
431
|
+
isInitialized.current = true;
|
|
432
|
+
|
|
433
|
+
const initEditor = async () => {
|
|
434
|
+
try {
|
|
435
|
+
const monaco = await init({
|
|
436
|
+
defaultTheme: theme,
|
|
437
|
+
// Pre-load SQL grammar for syntax highlighting
|
|
438
|
+
langs: ["sql"],
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
if (!containerRef.current) return;
|
|
442
|
+
|
|
443
|
+
// Create a model with the SQL language explicitly set
|
|
444
|
+
const model = monaco.editor.createModel(value, "sql");
|
|
445
|
+
|
|
446
|
+
const editor = monaco.editor.create(containerRef.current, {
|
|
447
|
+
model,
|
|
448
|
+
theme: theme,
|
|
449
|
+
automaticLayout: true,
|
|
450
|
+
minimap: { enabled: false },
|
|
451
|
+
scrollBeyondLastLine: false,
|
|
452
|
+
fontSize: 14,
|
|
453
|
+
fontFamily: "'JetBrains Mono', 'Fira Code', Consolas, Monaco, monospace",
|
|
454
|
+
lineNumbers: "on",
|
|
455
|
+
renderLineHighlight: "line",
|
|
456
|
+
wordWrap: "on",
|
|
457
|
+
padding: { top: 12, bottom: 12 },
|
|
458
|
+
readOnly: disabled,
|
|
459
|
+
tabSize: 2,
|
|
460
|
+
bracketPairColorization: { enabled: true },
|
|
461
|
+
folding: true,
|
|
462
|
+
glyphMargin: false,
|
|
463
|
+
lineNumbersMinChars: 3,
|
|
464
|
+
overviewRulerBorder: false,
|
|
465
|
+
// Autocomplete settings - press Ctrl+Space to trigger suggestions
|
|
466
|
+
quickSuggestions: true,
|
|
467
|
+
suggestOnTriggerCharacters: true,
|
|
468
|
+
wordBasedSuggestions: "off",
|
|
469
|
+
scrollbar: {
|
|
470
|
+
vertical: "auto",
|
|
471
|
+
horizontal: "auto",
|
|
472
|
+
verticalScrollbarSize: 10,
|
|
473
|
+
horizontalScrollbarSize: 10,
|
|
474
|
+
},
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
editorRef.current = editor;
|
|
478
|
+
monacoRef.current = monaco;
|
|
479
|
+
|
|
480
|
+
// Register completion provider
|
|
481
|
+
registerCompletionProvider(monaco);
|
|
482
|
+
|
|
483
|
+
// Listen for content changes
|
|
484
|
+
editor.onDidChangeModelContent(() => {
|
|
485
|
+
const newValue = editor.getValue();
|
|
486
|
+
onChangeRef.current?.(newValue);
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
// Ctrl+Enter to submit query
|
|
490
|
+
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, () => {
|
|
491
|
+
onSubmitRef.current?.();
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
// Handle focus/blur for accessibility
|
|
495
|
+
editor.onDidFocusEditorText(() => {
|
|
496
|
+
containerRef.current?.classList.add("editor-focused");
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
editor.onDidBlurEditorText(() => {
|
|
500
|
+
containerRef.current?.classList.remove("editor-focused");
|
|
501
|
+
});
|
|
502
|
+
} catch (error) {
|
|
503
|
+
console.error("Failed to initialize Monaco editor:", error);
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
initEditor();
|
|
508
|
+
|
|
509
|
+
return () => {
|
|
510
|
+
if (completionProviderRef.current) {
|
|
511
|
+
completionProviderRef.current.dispose();
|
|
512
|
+
completionProviderRef.current = null;
|
|
513
|
+
}
|
|
514
|
+
if (editorRef.current) {
|
|
515
|
+
editorRef.current.dispose();
|
|
516
|
+
editorRef.current = null;
|
|
517
|
+
}
|
|
518
|
+
monacoRef.current = null;
|
|
519
|
+
isInitialized.current = false;
|
|
520
|
+
};
|
|
521
|
+
// Only run once on mount - value updates handled separately
|
|
522
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
523
|
+
}, []);
|
|
524
|
+
|
|
525
|
+
// Helper function to register completion provider
|
|
526
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
527
|
+
const registerCompletionProvider = (monaco: any) => {
|
|
528
|
+
// Dispose previous provider if exists
|
|
529
|
+
if (completionProviderRef.current) {
|
|
530
|
+
completionProviderRef.current.dispose();
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Register new completion provider
|
|
534
|
+
completionProviderRef.current = monaco.languages.registerCompletionItemProvider("sql", {
|
|
535
|
+
triggerCharacters: [" ", ".", ",", "("],
|
|
536
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
537
|
+
provideCompletionItems: (model: any, position: any) => {
|
|
538
|
+
const word = model.getWordUntilPosition(position);
|
|
539
|
+
const range = {
|
|
540
|
+
startLineNumber: position.lineNumber,
|
|
541
|
+
endLineNumber: position.lineNumber,
|
|
542
|
+
startColumn: word.startColumn,
|
|
543
|
+
endColumn: word.endColumn,
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
const suggestions: Array<{
|
|
547
|
+
label: string;
|
|
548
|
+
kind: number;
|
|
549
|
+
insertText: string;
|
|
550
|
+
detail?: string;
|
|
551
|
+
range: typeof range;
|
|
552
|
+
sortText?: string;
|
|
553
|
+
}> = [];
|
|
554
|
+
|
|
555
|
+
// Add SQL keywords
|
|
556
|
+
for (const keyword of SQL_KEYWORDS) {
|
|
557
|
+
suggestions.push({
|
|
558
|
+
label: keyword,
|
|
559
|
+
kind: monaco.languages.CompletionItemKind.Keyword,
|
|
560
|
+
insertText: keyword,
|
|
561
|
+
detail: "SQL keyword",
|
|
562
|
+
range,
|
|
563
|
+
sortText: `2_${keyword}`, // Keywords sort after columns/tables
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Add table name - use schemaRef to get latest value
|
|
568
|
+
const currentSchema = schemaRef.current;
|
|
569
|
+
if (currentSchema) {
|
|
570
|
+
suggestions.push({
|
|
571
|
+
label: currentSchema.name,
|
|
572
|
+
kind: monaco.languages.CompletionItemKind.Class,
|
|
573
|
+
insertText: currentSchema.name,
|
|
574
|
+
detail: "Table",
|
|
575
|
+
range,
|
|
576
|
+
sortText: `0_${currentSchema.name}`, // Tables sort first
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
// Add column names
|
|
580
|
+
for (const col of currentSchema.columns) {
|
|
581
|
+
suggestions.push({
|
|
582
|
+
label: col.name,
|
|
583
|
+
kind: monaco.languages.CompletionItemKind.Field,
|
|
584
|
+
insertText: col.name,
|
|
585
|
+
detail: `${col.type || "TEXT"}${col.primaryKey ? " (PK)" : ""}${!col.nullable ? " NOT NULL" : ""}`,
|
|
586
|
+
range,
|
|
587
|
+
sortText: `1_${col.name}`, // Columns sort after tables, before keywords
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
} else {
|
|
591
|
+
// Fallback if schema not loaded - add site_events as table
|
|
592
|
+
suggestions.push({
|
|
593
|
+
label: "site_events",
|
|
594
|
+
kind: monaco.languages.CompletionItemKind.Class,
|
|
595
|
+
insertText: "site_events",
|
|
596
|
+
detail: "Table",
|
|
597
|
+
range,
|
|
598
|
+
sortText: "0_site_events",
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Add common SQL functions
|
|
603
|
+
const functions = [
|
|
604
|
+
{ name: "COUNT", detail: "Count rows" },
|
|
605
|
+
{ name: "SUM", detail: "Sum values" },
|
|
606
|
+
{ name: "AVG", detail: "Average value" },
|
|
607
|
+
{ name: "MIN", detail: "Minimum value" },
|
|
608
|
+
{ name: "MAX", detail: "Maximum value" },
|
|
609
|
+
{ name: "STRFTIME", detail: "Format date/time" },
|
|
610
|
+
{ name: "COALESCE", detail: "Return first non-null" },
|
|
611
|
+
{ name: "IFNULL", detail: "Replace null with value" },
|
|
612
|
+
{ name: "LENGTH", detail: "String length" },
|
|
613
|
+
{ name: "LOWER", detail: "Lowercase string" },
|
|
614
|
+
{ name: "UPPER", detail: "Uppercase string" },
|
|
615
|
+
{ name: "TRIM", detail: "Remove whitespace" },
|
|
616
|
+
{ name: "SUBSTR", detail: "Substring" },
|
|
617
|
+
{ name: "REPLACE", detail: "Replace text" },
|
|
618
|
+
{ name: "INSTR", detail: "Find position" },
|
|
619
|
+
{ name: "ABS", detail: "Absolute value" },
|
|
620
|
+
{ name: "ROUND", detail: "Round number" },
|
|
621
|
+
];
|
|
622
|
+
|
|
623
|
+
for (const fn of functions) {
|
|
624
|
+
suggestions.push({
|
|
625
|
+
label: fn.name,
|
|
626
|
+
kind: monaco.languages.CompletionItemKind.Function,
|
|
627
|
+
insertText: `${fn.name}()`,
|
|
628
|
+
detail: fn.detail,
|
|
629
|
+
range,
|
|
630
|
+
sortText: `3_${fn.name}`, // Functions sort after keywords
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
return { suggestions };
|
|
635
|
+
},
|
|
636
|
+
});
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
// Update editor value when prop changes (from external source)
|
|
640
|
+
useEffect(() => {
|
|
641
|
+
if (editorRef.current) {
|
|
642
|
+
const currentValue = editorRef.current.getValue();
|
|
643
|
+
if (currentValue !== value) {
|
|
644
|
+
editorRef.current.setValue(value);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}, [value]);
|
|
648
|
+
|
|
649
|
+
// Update read-only state when disabled changes
|
|
650
|
+
useEffect(() => {
|
|
651
|
+
if (editorRef.current) {
|
|
652
|
+
editorRef.current.updateOptions({ readOnly: disabled });
|
|
653
|
+
}
|
|
654
|
+
}, [disabled]);
|
|
655
|
+
|
|
656
|
+
// Update theme when it changes
|
|
657
|
+
useEffect(() => {
|
|
658
|
+
if (editorRef.current) {
|
|
659
|
+
init().then((monaco) => {
|
|
660
|
+
monaco.editor.setTheme(theme);
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
}, [theme]);
|
|
664
|
+
|
|
665
|
+
const handleSelectQuery = (query: string) => {
|
|
666
|
+
if (editorRef.current) {
|
|
667
|
+
editorRef.current.setValue(query);
|
|
668
|
+
onChangeRef.current?.(query);
|
|
669
|
+
}
|
|
670
|
+
setActiveTab("editor");
|
|
671
|
+
};
|
|
672
|
+
|
|
673
|
+
// If schema is disabled, just render the editor
|
|
674
|
+
if (!showSchema) {
|
|
675
|
+
return (
|
|
676
|
+
<div
|
|
677
|
+
ref={containerRef}
|
|
678
|
+
className={`sql-editor-container rounded-lg border border-[var(--theme-input-border)] bg-[var(--theme-input-bg)] overflow-hidden transition-all ${
|
|
679
|
+
disabled ? "opacity-60 cursor-not-allowed" : ""
|
|
680
|
+
} ${className}`}
|
|
681
|
+
style={{ height, minHeight: "120px" }}
|
|
682
|
+
/>
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
return (
|
|
687
|
+
<div className={`sql-editor-wrapper ${className}`}>
|
|
688
|
+
{/* Tabs */}
|
|
689
|
+
<div className="flex mb-2 bg-[var(--theme-bg-secondary)] rounded-lg p-1 w-fit">
|
|
690
|
+
<button
|
|
691
|
+
onClick={() => setActiveTab("editor")}
|
|
692
|
+
className={`px-4 py-1.5 text-xs font-medium rounded-md transition-colors ${
|
|
693
|
+
activeTab === "editor"
|
|
694
|
+
? "bg-[var(--theme-bg-primary)] text-[var(--theme-text-primary)] shadow-sm"
|
|
695
|
+
: "text-[var(--theme-text-secondary)] hover:text-[var(--theme-text-primary)]"
|
|
696
|
+
}`}
|
|
697
|
+
>
|
|
698
|
+
Editor
|
|
699
|
+
</button>
|
|
700
|
+
<button
|
|
701
|
+
onClick={() => setActiveTab("schema")}
|
|
702
|
+
className={`px-4 py-1.5 text-xs font-medium rounded-md transition-colors ${
|
|
703
|
+
activeTab === "schema"
|
|
704
|
+
? "bg-[var(--theme-bg-primary)] text-[var(--theme-text-primary)] shadow-sm"
|
|
705
|
+
: "text-[var(--theme-text-secondary)] hover:text-[var(--theme-text-primary)]"
|
|
706
|
+
}`}
|
|
707
|
+
>
|
|
708
|
+
Schema
|
|
709
|
+
</button>
|
|
710
|
+
</div>
|
|
711
|
+
|
|
712
|
+
{/* Editor Tab */}
|
|
713
|
+
<div style={{ display: activeTab === "editor" ? "block" : "none" }}>
|
|
714
|
+
<div
|
|
715
|
+
ref={containerRef}
|
|
716
|
+
className={`sql-editor-container rounded-lg border border-[var(--theme-input-border)] bg-[var(--theme-input-bg)] overflow-hidden transition-all ${
|
|
717
|
+
disabled ? "opacity-60 cursor-not-allowed" : ""
|
|
718
|
+
}`}
|
|
719
|
+
style={{ height, minHeight: "120px" }}
|
|
720
|
+
/>
|
|
721
|
+
<p className="text-xs text-[var(--theme-text-secondary)] mt-1.5 opacity-70">
|
|
722
|
+
<kbd className="px-1.5 py-0.5 rounded bg-[var(--theme-bg-secondary)] font-mono text-[10px]">Ctrl</kbd>+<kbd className="px-1.5 py-0.5 rounded bg-[var(--theme-bg-secondary)] font-mono text-[10px]">Space</kbd> autocomplete
|
|
723
|
+
<span className="mx-2">·</span>
|
|
724
|
+
<kbd className="px-1.5 py-0.5 rounded bg-[var(--theme-bg-secondary)] font-mono text-[10px]">Ctrl</kbd>+<kbd className="px-1.5 py-0.5 rounded bg-[var(--theme-bg-secondary)] font-mono text-[10px]">Enter</kbd> run query
|
|
725
|
+
</p>
|
|
726
|
+
</div>
|
|
727
|
+
|
|
728
|
+
{/* Schema Tab */}
|
|
729
|
+
{activeTab === "schema" && (
|
|
730
|
+
<SchemaViewer
|
|
731
|
+
onSelectQuery={handleSelectQuery}
|
|
732
|
+
height={height}
|
|
733
|
+
siteId={siteId}
|
|
734
|
+
/>
|
|
735
|
+
)}
|
|
736
|
+
</div>
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
export default SQLEditor;
|