@geekmidas/studio 0.0.1

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.
Files changed (64) hide show
  1. package/dist/DataBrowser-DQ3-ZxdV.mjs +427 -0
  2. package/dist/DataBrowser-DQ3-ZxdV.mjs.map +1 -0
  3. package/dist/DataBrowser-SOcqmZb2.d.mts +267 -0
  4. package/dist/DataBrowser-c-Gs6PZB.cjs +432 -0
  5. package/dist/DataBrowser-c-Gs6PZB.cjs.map +1 -0
  6. package/dist/DataBrowser-hGwiTffZ.d.cts +267 -0
  7. package/dist/chunk-CUT6urMc.cjs +30 -0
  8. package/dist/data/index.cjs +4 -0
  9. package/dist/data/index.d.cts +2 -0
  10. package/dist/data/index.d.mts +2 -0
  11. package/dist/data/index.mjs +4 -0
  12. package/dist/index.cjs +239 -0
  13. package/dist/index.cjs.map +1 -0
  14. package/dist/index.d.cts +132 -0
  15. package/dist/index.d.mts +132 -0
  16. package/dist/index.mjs +230 -0
  17. package/dist/index.mjs.map +1 -0
  18. package/dist/server/hono.cjs +192 -0
  19. package/dist/server/hono.cjs.map +1 -0
  20. package/dist/server/hono.d.cts +19 -0
  21. package/dist/server/hono.d.mts +19 -0
  22. package/dist/server/hono.mjs +191 -0
  23. package/dist/server/hono.mjs.map +1 -0
  24. package/dist/types-BZv87Ikv.mjs +31 -0
  25. package/dist/types-BZv87Ikv.mjs.map +1 -0
  26. package/dist/types-CMttUZYk.cjs +43 -0
  27. package/dist/types-CMttUZYk.cjs.map +1 -0
  28. package/package.json +54 -0
  29. package/src/Studio.ts +318 -0
  30. package/src/data/DataBrowser.ts +166 -0
  31. package/src/data/__tests__/DataBrowser.integration.spec.ts +418 -0
  32. package/src/data/__tests__/filtering.integration.spec.ts +741 -0
  33. package/src/data/__tests__/introspection.integration.spec.ts +352 -0
  34. package/src/data/filtering.ts +191 -0
  35. package/src/data/index.ts +1 -0
  36. package/src/data/introspection.ts +220 -0
  37. package/src/data/pagination.ts +33 -0
  38. package/src/index.ts +31 -0
  39. package/src/server/__tests__/hono.integration.spec.ts +361 -0
  40. package/src/server/hono.ts +225 -0
  41. package/src/types.ts +278 -0
  42. package/src/ui-assets.ts +40 -0
  43. package/tsdown.config.ts +13 -0
  44. package/ui/index.html +12 -0
  45. package/ui/node_modules/.bin/browserslist +21 -0
  46. package/ui/node_modules/.bin/jiti +21 -0
  47. package/ui/node_modules/.bin/terser +21 -0
  48. package/ui/node_modules/.bin/tsc +21 -0
  49. package/ui/node_modules/.bin/tsserver +21 -0
  50. package/ui/node_modules/.bin/tsx +21 -0
  51. package/ui/node_modules/.bin/vite +21 -0
  52. package/ui/package.json +24 -0
  53. package/ui/src/App.tsx +141 -0
  54. package/ui/src/api.ts +71 -0
  55. package/ui/src/components/RowDetail.tsx +113 -0
  56. package/ui/src/components/TableList.tsx +51 -0
  57. package/ui/src/components/TableView.tsx +219 -0
  58. package/ui/src/main.tsx +10 -0
  59. package/ui/src/styles.css +36 -0
  60. package/ui/src/types.ts +50 -0
  61. package/ui/src/vite-env.d.ts +1 -0
  62. package/ui/tsconfig.json +21 -0
  63. package/ui/tsconfig.tsbuildinfo +1 -0
  64. package/ui/vite.config.ts +12 -0
@@ -0,0 +1,13 @@
1
+ import { defineConfig } from 'tsdown';
2
+
3
+ export default defineConfig({
4
+ entry: ['src/index.ts', 'src/data/index.ts', 'src/server/hono.ts'],
5
+ clean: true,
6
+ outDir: 'dist',
7
+ format: ['cjs', 'esm'],
8
+ sourcemap: true,
9
+ dts: true,
10
+ outExtensions: (ctx) => ({
11
+ js: ctx.format === 'es' ? '.mjs' : '.cjs',
12
+ }),
13
+ });
package/ui/index.html ADDED
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Studio - Database Browser</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,21 @@
1
+ #!/bin/sh
2
+ basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
3
+
4
+ case `uname` in
5
+ *CYGWIN*|*MINGW*|*MSYS*)
6
+ if command -v cygpath > /dev/null 2>&1; then
7
+ basedir=`cygpath -w "$basedir"`
8
+ fi
9
+ ;;
10
+ esac
11
+
12
+ if [ -z "$NODE_PATH" ]; then
13
+ export NODE_PATH="/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/browserslist@4.25.2/node_modules/browserslist/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/browserslist@4.25.2/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/node_modules"
14
+ else
15
+ export NODE_PATH="/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/browserslist@4.25.2/node_modules/browserslist/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/browserslist@4.25.2/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/node_modules:$NODE_PATH"
16
+ fi
17
+ if [ -x "$basedir/node" ]; then
18
+ exec "$basedir/node" "$basedir/../../../../../node_modules/.pnpm/browserslist@4.25.2/node_modules/browserslist/cli.js" "$@"
19
+ else
20
+ exec node "$basedir/../../../../../node_modules/.pnpm/browserslist@4.25.2/node_modules/browserslist/cli.js" "$@"
21
+ fi
@@ -0,0 +1,21 @@
1
+ #!/bin/sh
2
+ basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
3
+
4
+ case `uname` in
5
+ *CYGWIN*|*MINGW*|*MSYS*)
6
+ if command -v cygpath > /dev/null 2>&1; then
7
+ basedir=`cygpath -w "$basedir"`
8
+ fi
9
+ ;;
10
+ esac
11
+
12
+ if [ -z "$NODE_PATH" ]; then
13
+ export NODE_PATH="/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/jiti@2.6.1/node_modules/jiti/lib/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/jiti@2.6.1/node_modules/jiti/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/jiti@2.6.1/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/node_modules"
14
+ else
15
+ export NODE_PATH="/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/jiti@2.6.1/node_modules/jiti/lib/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/jiti@2.6.1/node_modules/jiti/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/jiti@2.6.1/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/node_modules:$NODE_PATH"
16
+ fi
17
+ if [ -x "$basedir/node" ]; then
18
+ exec "$basedir/node" "$basedir/../../../../../node_modules/.pnpm/jiti@2.6.1/node_modules/jiti/lib/jiti-cli.mjs" "$@"
19
+ else
20
+ exec node "$basedir/../../../../../node_modules/.pnpm/jiti@2.6.1/node_modules/jiti/lib/jiti-cli.mjs" "$@"
21
+ fi
@@ -0,0 +1,21 @@
1
+ #!/bin/sh
2
+ basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
3
+
4
+ case `uname` in
5
+ *CYGWIN*|*MINGW*|*MSYS*)
6
+ if command -v cygpath > /dev/null 2>&1; then
7
+ basedir=`cygpath -w "$basedir"`
8
+ fi
9
+ ;;
10
+ esac
11
+
12
+ if [ -z "$NODE_PATH" ]; then
13
+ export NODE_PATH="/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/terser@5.43.1/node_modules/terser/bin/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/terser@5.43.1/node_modules/terser/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/terser@5.43.1/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/node_modules"
14
+ else
15
+ export NODE_PATH="/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/terser@5.43.1/node_modules/terser/bin/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/terser@5.43.1/node_modules/terser/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/terser@5.43.1/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/node_modules:$NODE_PATH"
16
+ fi
17
+ if [ -x "$basedir/node" ]; then
18
+ exec "$basedir/node" "$basedir/../../../../../node_modules/.pnpm/terser@5.43.1/node_modules/terser/bin/terser" "$@"
19
+ else
20
+ exec node "$basedir/../../../../../node_modules/.pnpm/terser@5.43.1/node_modules/terser/bin/terser" "$@"
21
+ fi
@@ -0,0 +1,21 @@
1
+ #!/bin/sh
2
+ basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
3
+
4
+ case `uname` in
5
+ *CYGWIN*|*MINGW*|*MSYS*)
6
+ if command -v cygpath > /dev/null 2>&1; then
7
+ basedir=`cygpath -w "$basedir"`
8
+ fi
9
+ ;;
10
+ esac
11
+
12
+ if [ -z "$NODE_PATH" ]; then
13
+ export NODE_PATH="/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/typescript@5.8.2/node_modules/typescript/bin/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/typescript@5.8.2/node_modules/typescript/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/typescript@5.8.2/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/node_modules"
14
+ else
15
+ export NODE_PATH="/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/typescript@5.8.2/node_modules/typescript/bin/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/typescript@5.8.2/node_modules/typescript/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/typescript@5.8.2/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/node_modules:$NODE_PATH"
16
+ fi
17
+ if [ -x "$basedir/node" ]; then
18
+ exec "$basedir/node" "$basedir/../typescript/bin/tsc" "$@"
19
+ else
20
+ exec node "$basedir/../typescript/bin/tsc" "$@"
21
+ fi
@@ -0,0 +1,21 @@
1
+ #!/bin/sh
2
+ basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
3
+
4
+ case `uname` in
5
+ *CYGWIN*|*MINGW*|*MSYS*)
6
+ if command -v cygpath > /dev/null 2>&1; then
7
+ basedir=`cygpath -w "$basedir"`
8
+ fi
9
+ ;;
10
+ esac
11
+
12
+ if [ -z "$NODE_PATH" ]; then
13
+ export NODE_PATH="/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/typescript@5.8.2/node_modules/typescript/bin/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/typescript@5.8.2/node_modules/typescript/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/typescript@5.8.2/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/node_modules"
14
+ else
15
+ export NODE_PATH="/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/typescript@5.8.2/node_modules/typescript/bin/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/typescript@5.8.2/node_modules/typescript/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/typescript@5.8.2/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/node_modules:$NODE_PATH"
16
+ fi
17
+ if [ -x "$basedir/node" ]; then
18
+ exec "$basedir/node" "$basedir/../typescript/bin/tsserver" "$@"
19
+ else
20
+ exec node "$basedir/../typescript/bin/tsserver" "$@"
21
+ fi
@@ -0,0 +1,21 @@
1
+ #!/bin/sh
2
+ basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
3
+
4
+ case `uname` in
5
+ *CYGWIN*|*MINGW*|*MSYS*)
6
+ if command -v cygpath > /dev/null 2>&1; then
7
+ basedir=`cygpath -w "$basedir"`
8
+ fi
9
+ ;;
10
+ esac
11
+
12
+ if [ -z "$NODE_PATH" ]; then
13
+ export NODE_PATH="/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/tsx@4.20.6/node_modules/tsx/dist/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/tsx@4.20.6/node_modules/tsx/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/tsx@4.20.6/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/node_modules"
14
+ else
15
+ export NODE_PATH="/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/tsx@4.20.6/node_modules/tsx/dist/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/tsx@4.20.6/node_modules/tsx/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/tsx@4.20.6/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/node_modules:$NODE_PATH"
16
+ fi
17
+ if [ -x "$basedir/node" ]; then
18
+ exec "$basedir/node" "$basedir/../../../../../node_modules/.pnpm/tsx@4.20.6/node_modules/tsx/dist/cli.mjs" "$@"
19
+ else
20
+ exec node "$basedir/../../../../../node_modules/.pnpm/tsx@4.20.6/node_modules/tsx/dist/cli.mjs" "$@"
21
+ fi
@@ -0,0 +1,21 @@
1
+ #!/bin/sh
2
+ basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
3
+
4
+ case `uname` in
5
+ *CYGWIN*|*MINGW*|*MSYS*)
6
+ if command -v cygpath > /dev/null 2>&1; then
7
+ basedir=`cygpath -w "$basedir"`
8
+ fi
9
+ ;;
10
+ esac
11
+
12
+ if [ -z "$NODE_PATH" ]; then
13
+ export NODE_PATH="/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/vite@6.3.5_@types+node@24.9.1_jiti@2.6.1_lightningcss@1.30.2_terser@5.43.1_tsx@4.20.6/node_modules/vite/bin/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/vite@6.3.5_@types+node@24.9.1_jiti@2.6.1_lightningcss@1.30.2_terser@5.43.1_tsx@4.20.6/node_modules/vite/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/vite@6.3.5_@types+node@24.9.1_jiti@2.6.1_lightningcss@1.30.2_terser@5.43.1_tsx@4.20.6/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/node_modules"
14
+ else
15
+ export NODE_PATH="/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/vite@6.3.5_@types+node@24.9.1_jiti@2.6.1_lightningcss@1.30.2_terser@5.43.1_tsx@4.20.6/node_modules/vite/bin/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/vite@6.3.5_@types+node@24.9.1_jiti@2.6.1_lightningcss@1.30.2_terser@5.43.1_tsx@4.20.6/node_modules/vite/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/vite@6.3.5_@types+node@24.9.1_jiti@2.6.1_lightningcss@1.30.2_terser@5.43.1_tsx@4.20.6/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/node_modules:$NODE_PATH"
16
+ fi
17
+ if [ -x "$basedir/node" ]; then
18
+ exec "$basedir/node" "$basedir/../vite/bin/vite.js" "$@"
19
+ else
20
+ exec node "$basedir/../vite/bin/vite.js" "$@"
21
+ fi
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@geekmidas/studio-ui",
3
+ "private": true,
4
+ "version": "0.0.1",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc -b && vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "react": "^19.0.0",
13
+ "react-dom": "^19.0.0"
14
+ },
15
+ "devDependencies": {
16
+ "@tailwindcss/vite": "^4.0.0",
17
+ "@types/react": "^19.0.0",
18
+ "@types/react-dom": "^19.0.0",
19
+ "@vitejs/plugin-react": "^4.3.4",
20
+ "tailwindcss": "^4.0.0",
21
+ "typescript": "~5.8.2",
22
+ "vite": "^6.0.0"
23
+ }
24
+ }
package/ui/src/App.tsx ADDED
@@ -0,0 +1,141 @@
1
+ import { useCallback, useEffect, useState } from 'react';
2
+ import * as api from './api';
3
+ import { RowDetail } from './components/RowDetail';
4
+ import { TableList } from './components/TableList';
5
+ import { TableView } from './components/TableView';
6
+ import type { TableInfo, TableSummary } from './types';
7
+
8
+ export function App() {
9
+ const [tables, setTables] = useState<TableSummary[]>([]);
10
+ const [selectedTable, setSelectedTable] = useState<string | null>(null);
11
+ const [tableInfo, setTableInfo] = useState<TableInfo | null>(null);
12
+ const [selectedRow, setSelectedRow] = useState<Record<
13
+ string,
14
+ unknown
15
+ > | null>(null);
16
+ const [loading, setLoading] = useState(true);
17
+ const [error, setError] = useState<string | null>(null);
18
+
19
+ // Load tables on mount
20
+ useEffect(() => {
21
+ async function loadTables() {
22
+ try {
23
+ const data = await api.getTables();
24
+ setTables(data.tables);
25
+ } catch (err) {
26
+ setError(err instanceof Error ? err.message : 'Failed to load tables');
27
+ } finally {
28
+ setLoading(false);
29
+ }
30
+ }
31
+ loadTables();
32
+ }, []);
33
+
34
+ // Load table info when selected
35
+ useEffect(() => {
36
+ if (!selectedTable) {
37
+ setTableInfo(null);
38
+ return;
39
+ }
40
+
41
+ async function loadTableInfo() {
42
+ try {
43
+ const info = await api.getTableInfo(selectedTable!);
44
+ setTableInfo(info);
45
+ } catch (err) {
46
+ setError(
47
+ err instanceof Error ? err.message : 'Failed to load table info',
48
+ );
49
+ }
50
+ }
51
+ loadTableInfo();
52
+ }, [selectedTable]);
53
+
54
+ const handleRefresh = useCallback(async () => {
55
+ setLoading(true);
56
+ try {
57
+ await api.getSchema(true);
58
+ const data = await api.getTables();
59
+ setTables(data.tables);
60
+ } catch (err) {
61
+ setError(err instanceof Error ? err.message : 'Failed to refresh');
62
+ } finally {
63
+ setLoading(false);
64
+ }
65
+ }, []);
66
+
67
+ const handleSelectTable = useCallback((tableName: string) => {
68
+ setSelectedTable(tableName);
69
+ setSelectedRow(null);
70
+ }, []);
71
+
72
+ const handleBack = useCallback(() => {
73
+ setSelectedTable(null);
74
+ setTableInfo(null);
75
+ setSelectedRow(null);
76
+ }, []);
77
+
78
+ return (
79
+ <div className="flex flex-col min-h-screen bg-bg-primary font-mono text-slate-100">
80
+ {/* Header */}
81
+ <header className="bg-bg-secondary border-b border-border px-6 py-4 flex items-center justify-between">
82
+ <div className="flex items-center gap-4">
83
+ {selectedTable && (
84
+ <button
85
+ onClick={handleBack}
86
+ className="text-slate-400 hover:text-slate-100 transition-colors"
87
+ >
88
+ &larr; Back
89
+ </button>
90
+ )}
91
+ <h1 className="text-xl font-semibold flex items-center gap-2">
92
+ <span className="text-purple-400">&#128451;</span> Studio
93
+ {selectedTable && (
94
+ <span className="text-slate-400">/ {selectedTable}</span>
95
+ )}
96
+ </h1>
97
+ </div>
98
+ <div className="flex items-center gap-4">
99
+ <span className="text-sm text-slate-400">{tables.length} tables</span>
100
+ <button
101
+ onClick={handleRefresh}
102
+ disabled={loading}
103
+ className="px-3 py-1 text-sm bg-bg-tertiary hover:bg-slate-600 rounded transition-colors disabled:opacity-50"
104
+ >
105
+ {loading ? 'Refreshing...' : 'Refresh'}
106
+ </button>
107
+ </div>
108
+ </header>
109
+
110
+ {/* Main Content */}
111
+ <main className="flex-1 flex overflow-hidden">
112
+ {error ? (
113
+ <div className="flex-1 flex items-center justify-center text-red-400">
114
+ {error}
115
+ </div>
116
+ ) : loading && tables.length === 0 ? (
117
+ <div className="flex-1 flex items-center justify-center text-slate-500">
118
+ Loading...
119
+ </div>
120
+ ) : !selectedTable ? (
121
+ <TableList tables={tables} onSelect={handleSelectTable} />
122
+ ) : (
123
+ <TableView
124
+ tableName={selectedTable}
125
+ tableInfo={tableInfo}
126
+ onRowSelect={setSelectedRow}
127
+ />
128
+ )}
129
+ </main>
130
+
131
+ {/* Row Detail Panel */}
132
+ {selectedRow && tableInfo && (
133
+ <RowDetail
134
+ row={selectedRow}
135
+ columns={tableInfo.columns}
136
+ onClose={() => setSelectedRow(null)}
137
+ />
138
+ )}
139
+ </div>
140
+ );
141
+ }
package/ui/src/api.ts ADDED
@@ -0,0 +1,71 @@
1
+ import type {
2
+ FilterConfig,
3
+ QueryResult,
4
+ SchemaInfo,
5
+ SortConfig,
6
+ TableInfo,
7
+ TableSummary,
8
+ } from './types';
9
+
10
+ const BASE_URL = '/__studio';
11
+
12
+ async function fetchJson<T>(url: string): Promise<T> {
13
+ const res = await fetch(`${BASE_URL}${url}`);
14
+ if (!res.ok) {
15
+ throw new Error(`HTTP ${res.status}: ${res.statusText}`);
16
+ }
17
+ return res.json();
18
+ }
19
+
20
+ export async function getSchema(refresh = false): Promise<SchemaInfo> {
21
+ const url = refresh ? '/api/schema?refresh=true' : '/api/schema';
22
+ return fetchJson(url);
23
+ }
24
+
25
+ export async function getTables(): Promise<{ tables: TableSummary[] }> {
26
+ return fetchJson('/api/tables');
27
+ }
28
+
29
+ export async function getTableInfo(tableName: string): Promise<TableInfo> {
30
+ return fetchJson(`/api/tables/${encodeURIComponent(tableName)}`);
31
+ }
32
+
33
+ export interface QueryOptions {
34
+ pageSize?: number;
35
+ cursor?: string;
36
+ filters?: FilterConfig[];
37
+ sort?: SortConfig[];
38
+ }
39
+
40
+ export async function queryTable(
41
+ tableName: string,
42
+ options: QueryOptions = {},
43
+ ): Promise<QueryResult> {
44
+ const params = new URLSearchParams();
45
+
46
+ if (options.pageSize) {
47
+ params.set('pageSize', String(options.pageSize));
48
+ }
49
+
50
+ if (options.cursor) {
51
+ params.set('cursor', options.cursor);
52
+ }
53
+
54
+ if (options.filters) {
55
+ for (const filter of options.filters) {
56
+ params.set(`filter[${filter.column}][${filter.operator}]`, filter.value);
57
+ }
58
+ }
59
+
60
+ if (options.sort && options.sort.length > 0) {
61
+ const sortStr = options.sort
62
+ .map((s) => `${s.column}:${s.direction}`)
63
+ .join(',');
64
+ params.set('sort', sortStr);
65
+ }
66
+
67
+ const queryStr = params.toString();
68
+ const url = `/api/tables/${encodeURIComponent(tableName)}/rows${queryStr ? `?${queryStr}` : ''}`;
69
+
70
+ return fetchJson(url);
71
+ }
@@ -0,0 +1,113 @@
1
+ import type { ColumnInfo } from '../types';
2
+
3
+ interface RowDetailProps {
4
+ row: Record<string, unknown>;
5
+ columns: ColumnInfo[];
6
+ onClose: () => void;
7
+ }
8
+
9
+ export function RowDetail({ row, columns, onClose }: RowDetailProps) {
10
+ const formatValue = (value: unknown): string => {
11
+ if (value === null) return 'NULL';
12
+ if (value === undefined) return '';
13
+ if (typeof value === 'boolean') return value ? 'true' : 'false';
14
+ if (typeof value === 'object') return JSON.stringify(value, null, 2);
15
+ return String(value);
16
+ };
17
+
18
+ const getValueClass = (value: unknown): string => {
19
+ if (value === null) return 'text-slate-500 italic';
20
+ if (typeof value === 'boolean')
21
+ return value ? 'text-green-400' : 'text-red-400';
22
+ if (typeof value === 'number') return 'text-blue-400';
23
+ if (typeof value === 'string' && value.length > 100)
24
+ return 'text-slate-300 text-xs';
25
+ return 'text-slate-300';
26
+ };
27
+
28
+ return (
29
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
30
+ <div className="bg-bg-secondary border border-border rounded-lg shadow-xl max-w-2xl w-full max-h-[80vh] flex flex-col">
31
+ {/* Header */}
32
+ <div className="flex items-center justify-between px-4 py-3 border-b border-border">
33
+ <h2 className="text-lg font-semibold text-slate-100">Row Details</h2>
34
+ <button
35
+ onClick={onClose}
36
+ className="text-slate-400 hover:text-slate-100 transition-colors text-xl"
37
+ >
38
+ &times;
39
+ </button>
40
+ </div>
41
+
42
+ {/* Content */}
43
+ <div className="flex-1 overflow-y-auto p-4">
44
+ <div className="space-y-4">
45
+ {columns.map((col) => {
46
+ const value = row[col.name];
47
+ const isLongValue =
48
+ typeof value === 'string' && value.length > 100;
49
+ const isJson = typeof value === 'object' && value !== null;
50
+
51
+ return (
52
+ <div key={col.name} className="border-b border-border pb-3">
53
+ {/* Column name and metadata */}
54
+ <div className="flex items-center gap-2 mb-1">
55
+ <span className="font-medium text-slate-200">
56
+ {col.name}
57
+ </span>
58
+ <span className="text-xs text-slate-500">
59
+ {col.rawType}
60
+ </span>
61
+ {col.isPrimaryKey && (
62
+ <span className="text-xs text-amber-400 bg-amber-400/10 px-1.5 py-0.5 rounded">
63
+ PK
64
+ </span>
65
+ )}
66
+ {col.isForeignKey && (
67
+ <span className="text-xs text-blue-400 bg-blue-400/10 px-1.5 py-0.5 rounded">
68
+ FK
69
+ </span>
70
+ )}
71
+ {col.nullable && (
72
+ <span className="text-xs text-slate-500">nullable</span>
73
+ )}
74
+ </div>
75
+
76
+ {/* Value */}
77
+ {isLongValue || isJson ? (
78
+ <pre
79
+ className={`${getValueClass(value)} bg-bg-tertiary p-2 rounded overflow-x-auto whitespace-pre-wrap break-words`}
80
+ >
81
+ {formatValue(value)}
82
+ </pre>
83
+ ) : (
84
+ <div className={getValueClass(value)}>
85
+ {formatValue(value)}
86
+ </div>
87
+ )}
88
+
89
+ {/* Foreign key reference */}
90
+ {col.isForeignKey && col.foreignKeyTable && (
91
+ <div className="mt-1 text-xs text-blue-400">
92
+ References: {col.foreignKeyTable}.{col.foreignKeyColumn}
93
+ </div>
94
+ )}
95
+ </div>
96
+ );
97
+ })}
98
+ </div>
99
+ </div>
100
+
101
+ {/* Footer */}
102
+ <div className="px-4 py-3 border-t border-border flex justify-end">
103
+ <button
104
+ onClick={onClose}
105
+ className="px-4 py-2 bg-bg-tertiary hover:bg-slate-600 rounded transition-colors text-sm"
106
+ >
107
+ Close
108
+ </button>
109
+ </div>
110
+ </div>
111
+ </div>
112
+ );
113
+ }
@@ -0,0 +1,51 @@
1
+ import type { TableSummary } from '../types';
2
+
3
+ interface TableListProps {
4
+ tables: TableSummary[];
5
+ onSelect: (tableName: string) => void;
6
+ }
7
+
8
+ export function TableList({ tables, onSelect }: TableListProps) {
9
+ if (tables.length === 0) {
10
+ return (
11
+ <div className="flex-1 flex flex-col items-center justify-center text-slate-500">
12
+ <h3 className="text-lg mb-2">No tables found</h3>
13
+ <p className="text-sm">
14
+ Make sure your database has tables in the public schema.
15
+ </p>
16
+ </div>
17
+ );
18
+ }
19
+
20
+ return (
21
+ <div className="flex-1 p-4 overflow-y-auto">
22
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
23
+ {tables.map((table) => (
24
+ <div
25
+ key={table.name}
26
+ onClick={() => onSelect(table.name)}
27
+ className="bg-bg-secondary border border-border rounded-lg p-4 cursor-pointer transition-colors hover:border-purple-500 hover:bg-bg-tertiary"
28
+ >
29
+ <div className="flex items-center justify-between mb-2">
30
+ <h3 className="font-semibold text-slate-100 truncate">
31
+ {table.name}
32
+ </h3>
33
+ <span className="text-xs text-slate-500">{table.schema}</span>
34
+ </div>
35
+ <div className="flex items-center gap-4 text-sm text-slate-400">
36
+ <span>{table.columnCount} columns</span>
37
+ {table.estimatedRowCount !== undefined && (
38
+ <span>~{table.estimatedRowCount.toLocaleString()} rows</span>
39
+ )}
40
+ </div>
41
+ {table.primaryKey.length > 0 && (
42
+ <div className="mt-2 text-xs text-slate-500">
43
+ PK: {table.primaryKey.join(', ')}
44
+ </div>
45
+ )}
46
+ </div>
47
+ ))}
48
+ </div>
49
+ </div>
50
+ );
51
+ }