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,312 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useContext, useState } from "react";
|
|
4
|
+
import { Card } from "@/app/components/ui/Card";
|
|
5
|
+
import { Button } from "@/app/components/ui/Button";
|
|
6
|
+
import { SiteSelector } from "@components/SiteSelector";
|
|
7
|
+
import { AuthContext } from "@/app/providers/AuthProvider";
|
|
8
|
+
import { SQLEditor } from "@components/SQLEditor";
|
|
9
|
+
|
|
10
|
+
type ExploreInitialSite = {
|
|
11
|
+
site_id: number;
|
|
12
|
+
name: string;
|
|
13
|
+
tag_id: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type ExplorePageProps = {
|
|
17
|
+
initialSites?: ExploreInitialSite[];
|
|
18
|
+
initialSiteId?: number | null;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** Convert rows to CSV format */
|
|
22
|
+
function rowsToCsv(rows: Record<string, unknown>[]): string {
|
|
23
|
+
if (rows.length === 0) return "";
|
|
24
|
+
|
|
25
|
+
const headers = Object.keys(rows[0]);
|
|
26
|
+
const csvRows: string[] = [];
|
|
27
|
+
|
|
28
|
+
// Header row
|
|
29
|
+
csvRows.push(headers.map(h => `"${h.replace(/"/g, '""')}"`).join(","));
|
|
30
|
+
|
|
31
|
+
// Data rows
|
|
32
|
+
for (const row of rows) {
|
|
33
|
+
const values = headers.map(h => {
|
|
34
|
+
const val = row[h];
|
|
35
|
+
if (val === null || val === undefined) return "";
|
|
36
|
+
if (typeof val === "object") return `"${JSON.stringify(val).replace(/"/g, '""')}"`;
|
|
37
|
+
if (typeof val === "string") return `"${val.replace(/"/g, '""')}"`;
|
|
38
|
+
return String(val);
|
|
39
|
+
});
|
|
40
|
+
csvRows.push(values.join(","));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return csvRows.join("\n");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Convert rows to SQL INSERT statements */
|
|
47
|
+
function rowsToSql(rows: Record<string, unknown>[], tableName = "site_events"): string {
|
|
48
|
+
if (rows.length === 0) return "";
|
|
49
|
+
|
|
50
|
+
const headers = Object.keys(rows[0]);
|
|
51
|
+
const statements: string[] = [];
|
|
52
|
+
|
|
53
|
+
for (const row of rows) {
|
|
54
|
+
const values = headers.map(h => {
|
|
55
|
+
const val = row[h];
|
|
56
|
+
if (val === null || val === undefined) return "NULL";
|
|
57
|
+
if (typeof val === "object") return `'${JSON.stringify(val).replace(/'/g, "''")}'`;
|
|
58
|
+
if (typeof val === "string") return `'${val.replace(/'/g, "''")}'`;
|
|
59
|
+
if (typeof val === "number") return String(val);
|
|
60
|
+
return `'${String(val).replace(/'/g, "''")}'`;
|
|
61
|
+
});
|
|
62
|
+
statements.push(`INSERT INTO ${tableName} (${headers.join(", ")}) VALUES (${values.join(", ")});`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return statements.join("\n");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Download data as a file */
|
|
69
|
+
function downloadFile(content: string, filename: string, mimeType: string) {
|
|
70
|
+
const blob = new Blob([content], { type: mimeType });
|
|
71
|
+
const url = URL.createObjectURL(blob);
|
|
72
|
+
const a = document.createElement("a");
|
|
73
|
+
a.href = url;
|
|
74
|
+
a.download = filename;
|
|
75
|
+
document.body.appendChild(a);
|
|
76
|
+
a.click();
|
|
77
|
+
document.body.removeChild(a);
|
|
78
|
+
URL.revokeObjectURL(url);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function ExplorePage({ initialSites = [], initialSiteId = null }: ExplorePageProps) {
|
|
82
|
+
const {
|
|
83
|
+
current_site,
|
|
84
|
+
} = useContext(AuthContext) || {
|
|
85
|
+
current_site: null,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const currentSiteId = current_site?.id ?? initialSiteId;
|
|
89
|
+
|
|
90
|
+
const [sqlQuery, setSqlQuery] = useState("");
|
|
91
|
+
const [sqlStatus, setSqlStatus] = useState<"idle" | "loading" | "success" | "error">("idle");
|
|
92
|
+
const [sqlError, setSqlError] = useState<string | null>(null);
|
|
93
|
+
const [sqlResult, setSqlResult] = useState<null | {
|
|
94
|
+
rowCount: number;
|
|
95
|
+
rows: Record<string, unknown>[];
|
|
96
|
+
}>(null);
|
|
97
|
+
|
|
98
|
+
async function runSqlQuery() {
|
|
99
|
+
try {
|
|
100
|
+
setSqlError(null);
|
|
101
|
+
setSqlStatus("loading");
|
|
102
|
+
setSqlResult(null);
|
|
103
|
+
|
|
104
|
+
if (!currentSiteId) {
|
|
105
|
+
setSqlStatus("error");
|
|
106
|
+
setSqlError("Select a site first.");
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!sqlQuery.trim()) {
|
|
111
|
+
setSqlStatus("error");
|
|
112
|
+
setSqlError("Enter a SQL query to run.");
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const response = await fetch("/api/site-events/query", {
|
|
117
|
+
method: "POST",
|
|
118
|
+
headers: { "Content-Type": "application/json" },
|
|
119
|
+
body: JSON.stringify({
|
|
120
|
+
site_id: currentSiteId,
|
|
121
|
+
query: sqlQuery,
|
|
122
|
+
}),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const data = (await response.json().catch(() => null)) as
|
|
126
|
+
| { error?: string; rows?: Record<string, unknown>[]; rowCount?: number }
|
|
127
|
+
| null;
|
|
128
|
+
|
|
129
|
+
if (!response.ok) {
|
|
130
|
+
setSqlStatus("error");
|
|
131
|
+
setSqlError(data?.error || response.statusText || "Query failed");
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
setSqlResult({
|
|
136
|
+
rows: data?.rows ?? [],
|
|
137
|
+
rowCount: data?.rowCount ?? 0,
|
|
138
|
+
});
|
|
139
|
+
setSqlStatus("success");
|
|
140
|
+
} catch (error) {
|
|
141
|
+
console.error("SQL query failed", error);
|
|
142
|
+
setSqlStatus("error");
|
|
143
|
+
setSqlError("Query failed");
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<div className="p-6 bg-[var(--theme-bg-primary)] min-h-screen">
|
|
149
|
+
<div className="max-w-6xl mx-auto space-y-6">
|
|
150
|
+
<div>
|
|
151
|
+
<h1 className="text-3xl font-bold text-[var(--theme-text-primary)] mb-2">
|
|
152
|
+
Explore
|
|
153
|
+
</h1>
|
|
154
|
+
<p className="text-[var(--theme-text-secondary)]">
|
|
155
|
+
Query your site's event data directly using SQL. Use the Schema tab to see available columns and try example queries.
|
|
156
|
+
</p>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
{/* Site Selector */}
|
|
160
|
+
<Card className="p-4">
|
|
161
|
+
<div className="flex items-center gap-4">
|
|
162
|
+
<span className="text-sm font-medium text-[var(--theme-text-primary)]">
|
|
163
|
+
Site:
|
|
164
|
+
</span>
|
|
165
|
+
<SiteSelector initialSites={initialSites} initialSiteId={initialSiteId} />
|
|
166
|
+
</div>
|
|
167
|
+
</Card>
|
|
168
|
+
|
|
169
|
+
{/* SQL Explorer */}
|
|
170
|
+
<Card className="p-6">
|
|
171
|
+
<div className="flex items-center justify-between mb-4">
|
|
172
|
+
<h2 className="text-xl font-semibold text-[var(--theme-text-primary)]">
|
|
173
|
+
SQL Explorer
|
|
174
|
+
</h2>
|
|
175
|
+
<Button
|
|
176
|
+
variant="secondary"
|
|
177
|
+
onClick={() => {
|
|
178
|
+
setSqlResult(null);
|
|
179
|
+
setSqlError(null);
|
|
180
|
+
setSqlStatus("idle");
|
|
181
|
+
setSqlQuery("");
|
|
182
|
+
}}
|
|
183
|
+
disabled={sqlStatus === "loading"}
|
|
184
|
+
>
|
|
185
|
+
Clear
|
|
186
|
+
</Button>
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
<p className="text-sm text-[var(--theme-text-secondary)] mb-4">
|
|
190
|
+
Run read-only SQL queries against your site's event data. Check the Schema tab for available columns and indexes.
|
|
191
|
+
</p>
|
|
192
|
+
|
|
193
|
+
<label className="block text-xs font-medium text-[var(--theme-text-secondary)] mb-2">
|
|
194
|
+
Query (SELECT only)
|
|
195
|
+
</label>
|
|
196
|
+
<SQLEditor
|
|
197
|
+
value={sqlQuery}
|
|
198
|
+
onChange={setSqlQuery}
|
|
199
|
+
onSubmit={() => void runSqlQuery()}
|
|
200
|
+
placeholder="SELECT event, page_url, created_at FROM site_events ORDER BY created_at DESC LIMIT 50"
|
|
201
|
+
disabled={sqlStatus === "loading"}
|
|
202
|
+
height="400px"
|
|
203
|
+
siteId={currentSiteId ?? undefined}
|
|
204
|
+
/>
|
|
205
|
+
|
|
206
|
+
<div className="flex items-center justify-between mt-4">
|
|
207
|
+
<p className="text-xs text-[var(--theme-text-secondary)]">
|
|
208
|
+
Results are limited to 500 rows.
|
|
209
|
+
</p>
|
|
210
|
+
<Button
|
|
211
|
+
variant="primary"
|
|
212
|
+
onClick={() => void runSqlQuery()}
|
|
213
|
+
disabled={sqlStatus === "loading" || !currentSiteId}
|
|
214
|
+
>
|
|
215
|
+
{sqlStatus === "loading" ? "Running…" : "Run Query"}
|
|
216
|
+
</Button>
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
{sqlError && (
|
|
220
|
+
<div className="mt-4 p-3 rounded border border-red-500 text-red-400 bg-red-500/10">
|
|
221
|
+
{sqlError}
|
|
222
|
+
</div>
|
|
223
|
+
)}
|
|
224
|
+
|
|
225
|
+
{sqlResult && (
|
|
226
|
+
<div className="mt-4 space-y-3">
|
|
227
|
+
<div className="flex items-center justify-between">
|
|
228
|
+
<div className="text-sm text-[var(--theme-text-secondary)]">
|
|
229
|
+
Rows returned: <span className="font-medium text-[var(--theme-text-primary)]">{sqlResult.rowCount}</span>
|
|
230
|
+
</div>
|
|
231
|
+
{sqlResult.rows.length > 0 && (
|
|
232
|
+
<div className="flex items-center gap-2">
|
|
233
|
+
<span className="text-xs text-[var(--theme-text-secondary)]">Export:</span>
|
|
234
|
+
<button
|
|
235
|
+
onClick={() => {
|
|
236
|
+
const csv = rowsToCsv(sqlResult.rows);
|
|
237
|
+
downloadFile(csv, `query-results-${Date.now()}.csv`, "text/csv");
|
|
238
|
+
}}
|
|
239
|
+
className="px-2 py-1 text-xs font-medium rounded bg-[var(--theme-bg-secondary)] text-[var(--theme-text-secondary)] hover:text-[var(--theme-text-primary)] hover:bg-[var(--theme-input-border)] transition-colors"
|
|
240
|
+
>
|
|
241
|
+
CSV
|
|
242
|
+
</button>
|
|
243
|
+
<button
|
|
244
|
+
onClick={() => {
|
|
245
|
+
const json = JSON.stringify(sqlResult.rows, null, 2);
|
|
246
|
+
downloadFile(json, `query-results-${Date.now()}.json`, "application/json");
|
|
247
|
+
}}
|
|
248
|
+
className="px-2 py-1 text-xs font-medium rounded bg-[var(--theme-bg-secondary)] text-[var(--theme-text-secondary)] hover:text-[var(--theme-text-primary)] hover:bg-[var(--theme-input-border)] transition-colors"
|
|
249
|
+
>
|
|
250
|
+
JSON
|
|
251
|
+
</button>
|
|
252
|
+
<button
|
|
253
|
+
onClick={() => {
|
|
254
|
+
const sql = rowsToSql(sqlResult.rows);
|
|
255
|
+
downloadFile(sql, `query-results-${Date.now()}.sql`, "text/plain");
|
|
256
|
+
}}
|
|
257
|
+
className="px-2 py-1 text-xs font-medium rounded bg-[var(--theme-bg-secondary)] text-[var(--theme-text-secondary)] hover:text-[var(--theme-text-primary)] hover:bg-[var(--theme-input-border)] transition-colors"
|
|
258
|
+
>
|
|
259
|
+
SQL
|
|
260
|
+
</button>
|
|
261
|
+
</div>
|
|
262
|
+
)}
|
|
263
|
+
</div>
|
|
264
|
+
{sqlResult.rows.length > 0 ? (
|
|
265
|
+
<div className="overflow-x-auto border border-[var(--theme-border-primary)] rounded-lg">
|
|
266
|
+
<table className="min-w-full divide-y divide-[var(--theme-border-primary)]">
|
|
267
|
+
<thead className="bg-[var(--theme-card-bg)]">
|
|
268
|
+
<tr>
|
|
269
|
+
{Object.keys(sqlResult.rows[0]).map((key) => (
|
|
270
|
+
<th
|
|
271
|
+
key={key}
|
|
272
|
+
className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-[var(--theme-text-secondary)]"
|
|
273
|
+
>
|
|
274
|
+
{key}
|
|
275
|
+
</th>
|
|
276
|
+
))}
|
|
277
|
+
</tr>
|
|
278
|
+
</thead>
|
|
279
|
+
<tbody className="divide-y divide-[var(--theme-border-primary)]">
|
|
280
|
+
{sqlResult.rows.slice(0, 100).map((row, rowIndex) => (
|
|
281
|
+
<tr key={rowIndex} className="hover:bg-[var(--theme-bg-secondary)]">
|
|
282
|
+
{Object.keys(sqlResult.rows[0]).map((key) => (
|
|
283
|
+
<td key={key} className="px-4 py-3 text-sm whitespace-pre-wrap text-[var(--theme-text-primary)]">
|
|
284
|
+
{row[key] === null || row[key] === undefined
|
|
285
|
+
? <span className="text-[var(--theme-text-secondary)]">—</span>
|
|
286
|
+
: typeof row[key] === "object"
|
|
287
|
+
? JSON.stringify(row[key])
|
|
288
|
+
: String(row[key])}
|
|
289
|
+
</td>
|
|
290
|
+
))}
|
|
291
|
+
</tr>
|
|
292
|
+
))}
|
|
293
|
+
</tbody>
|
|
294
|
+
</table>
|
|
295
|
+
</div>
|
|
296
|
+
) : (
|
|
297
|
+
<p className="text-sm text-[var(--theme-text-secondary)]">No rows returned.</p>
|
|
298
|
+
)}
|
|
299
|
+
{sqlResult.rows.length > 100 && (
|
|
300
|
+
<p className="text-xs text-[var(--theme-text-secondary)]">
|
|
301
|
+
Showing first 100 rows of {sqlResult.rowCount}.
|
|
302
|
+
</p>
|
|
303
|
+
)}
|
|
304
|
+
</div>
|
|
305
|
+
)}
|
|
306
|
+
</Card>
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export default ExplorePage;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { LayoutProps } from "rwsdk/router";
|
|
2
|
+
import type { ContentItem } from "blinkx.io-sveltekit/dist/src/types/content";
|
|
3
|
+
import { Nav } from "@/app/components/Nav";
|
|
4
|
+
import type { NavInitialSession } from "@/app/components/Nav";
|
|
5
|
+
import { ClientProviders } from "@/app/providers/ClientProviders";
|
|
6
|
+
import type { RequestInfo } from "rwsdk/worker";
|
|
7
|
+
import type { AppContext } from "@/types/app-context";
|
|
8
|
+
|
|
9
|
+
export function AppLayout({
|
|
10
|
+
children,
|
|
11
|
+
requestInfo
|
|
12
|
+
}: LayoutProps<RequestInfo<any, AppContext>>) {
|
|
13
|
+
const dashboardPrefetchHref = "/dashboard/settings";
|
|
14
|
+
const session = requestInfo?.ctx?.session;
|
|
15
|
+
const initialNavSession: NavInitialSession | null = session
|
|
16
|
+
? {
|
|
17
|
+
user: {
|
|
18
|
+
name: session.user?.name ?? null,
|
|
19
|
+
email: session.user?.email ?? null,
|
|
20
|
+
image: session.user?.image ?? null,
|
|
21
|
+
},
|
|
22
|
+
team: session.team
|
|
23
|
+
? {
|
|
24
|
+
id: session.team.id,
|
|
25
|
+
name: session.team.name ?? null,
|
|
26
|
+
external_id: session.team.external_id ?? null,
|
|
27
|
+
}
|
|
28
|
+
: null,
|
|
29
|
+
all_teams: Array.isArray(session.all_teams)
|
|
30
|
+
? session.all_teams.map((team) => ({
|
|
31
|
+
id: team.id,
|
|
32
|
+
name: team.name ?? null,
|
|
33
|
+
external_id: team.external_id ?? null,
|
|
34
|
+
}))
|
|
35
|
+
: [],
|
|
36
|
+
}
|
|
37
|
+
: null;
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
//NOTE: This was a way to get all the client side providers to work
|
|
41
|
+
<ClientProviders>
|
|
42
|
+
{session
|
|
43
|
+
? <link rel="x-prefetch" href={dashboardPrefetchHref} />
|
|
44
|
+
: null}
|
|
45
|
+
<main className="w-full max-w-[1400px] mx-auto">
|
|
46
|
+
<Nav initialSession={initialNavSession} />
|
|
47
|
+
{children}
|
|
48
|
+
</main>
|
|
49
|
+
</ClientProviders>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const Page = (props: { content: ContentItem["body"] }) => (
|
|
54
|
+
<>
|
|
55
|
+
<style dangerouslySetInnerHTML={{ __html: props.content.css }} />
|
|
56
|
+
<div dangerouslySetInnerHTML={{ __html: props.content.html }} />
|
|
57
|
+
</>
|
|
58
|
+
);
|