@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.
- package/dist/DataBrowser-DQ3-ZxdV.mjs +427 -0
- package/dist/DataBrowser-DQ3-ZxdV.mjs.map +1 -0
- package/dist/DataBrowser-SOcqmZb2.d.mts +267 -0
- package/dist/DataBrowser-c-Gs6PZB.cjs +432 -0
- package/dist/DataBrowser-c-Gs6PZB.cjs.map +1 -0
- package/dist/DataBrowser-hGwiTffZ.d.cts +267 -0
- package/dist/chunk-CUT6urMc.cjs +30 -0
- package/dist/data/index.cjs +4 -0
- package/dist/data/index.d.cts +2 -0
- package/dist/data/index.d.mts +2 -0
- package/dist/data/index.mjs +4 -0
- package/dist/index.cjs +239 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +132 -0
- package/dist/index.d.mts +132 -0
- package/dist/index.mjs +230 -0
- package/dist/index.mjs.map +1 -0
- package/dist/server/hono.cjs +192 -0
- package/dist/server/hono.cjs.map +1 -0
- package/dist/server/hono.d.cts +19 -0
- package/dist/server/hono.d.mts +19 -0
- package/dist/server/hono.mjs +191 -0
- package/dist/server/hono.mjs.map +1 -0
- package/dist/types-BZv87Ikv.mjs +31 -0
- package/dist/types-BZv87Ikv.mjs.map +1 -0
- package/dist/types-CMttUZYk.cjs +43 -0
- package/dist/types-CMttUZYk.cjs.map +1 -0
- package/package.json +54 -0
- package/src/Studio.ts +318 -0
- package/src/data/DataBrowser.ts +166 -0
- package/src/data/__tests__/DataBrowser.integration.spec.ts +418 -0
- package/src/data/__tests__/filtering.integration.spec.ts +741 -0
- package/src/data/__tests__/introspection.integration.spec.ts +352 -0
- package/src/data/filtering.ts +191 -0
- package/src/data/index.ts +1 -0
- package/src/data/introspection.ts +220 -0
- package/src/data/pagination.ts +33 -0
- package/src/index.ts +31 -0
- package/src/server/__tests__/hono.integration.spec.ts +361 -0
- package/src/server/hono.ts +225 -0
- package/src/types.ts +278 -0
- package/src/ui-assets.ts +40 -0
- package/tsdown.config.ts +13 -0
- package/ui/index.html +12 -0
- package/ui/node_modules/.bin/browserslist +21 -0
- package/ui/node_modules/.bin/jiti +21 -0
- package/ui/node_modules/.bin/terser +21 -0
- package/ui/node_modules/.bin/tsc +21 -0
- package/ui/node_modules/.bin/tsserver +21 -0
- package/ui/node_modules/.bin/tsx +21 -0
- package/ui/node_modules/.bin/vite +21 -0
- package/ui/package.json +24 -0
- package/ui/src/App.tsx +141 -0
- package/ui/src/api.ts +71 -0
- package/ui/src/components/RowDetail.tsx +113 -0
- package/ui/src/components/TableList.tsx +51 -0
- package/ui/src/components/TableView.tsx +219 -0
- package/ui/src/main.tsx +10 -0
- package/ui/src/styles.css +36 -0
- package/ui/src/types.ts +50 -0
- package/ui/src/vite-env.d.ts +1 -0
- package/ui/tsconfig.json +21 -0
- package/ui/tsconfig.tsbuildinfo +1 -0
- package/ui/vite.config.ts +12 -0
package/tsdown.config.ts
ADDED
|
@@ -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
|
package/ui/package.json
ADDED
|
@@ -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
|
+
← Back
|
|
89
|
+
</button>
|
|
90
|
+
)}
|
|
91
|
+
<h1 className="text-xl font-semibold flex items-center gap-2">
|
|
92
|
+
<span className="text-purple-400">🗃</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
|
+
×
|
|
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
|
+
}
|