From 2a52a09909ccb99c658cd4653bcd112408bfb5c7 Mon Sep 17 00:00:00 2001 From: kraysent Date: Tue, 18 Aug 2026 12:40:24 +0100 Subject: [PATCH 1/2] Add SQL page to admin interface --- apps/admin/src/App.tsx | 6 ++ apps/admin/src/pages/SqlQuery.tsx | 58 +++++++++++++++++++ .../src/pages/catalog/CatalogSqlPanel.tsx | 8 ++- apps/admin/src/pages/index.ts | 1 + packages/lib/src/ui/SqlQueryEmbed.tsx | 6 +- 5 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 apps/admin/src/pages/SqlQuery.tsx diff --git a/apps/admin/src/App.tsx b/apps/admin/src/App.tsx index 5d575d2..f202078 100644 --- a/apps/admin/src/App.tsx +++ b/apps/admin/src/App.tsx @@ -9,6 +9,7 @@ import { import { MdAdminPanelSettings, MdAccountTree, + MdCode, MdOpenInNew, MdPublic, MdTableChart, @@ -20,6 +21,7 @@ import { DataCatalogPage, LoginPage, RecordCrossmatchDetailsPage, + SqlQueryPage, TableDetailsPage, TablesPage, } from "./pages"; @@ -101,6 +103,9 @@ function Layout() { + + + @@ -125,6 +130,7 @@ function App() { } /> } /> } /> + } /> } /> } /> { + const response = await tapSync({ + client: adminClient, + query: { query: sql }, + }); + if (response.error) { + throw new Error(formatApiError(response.error)); + } + if (!response.data?.data) { + throw new Error("No data received from server"); + } + return response.data.data; +} + +export function SqlQueryPage(): ReactElement { + const [searchParams, setSearchParams] = useSearchParams(); + const permalinkSql = searchParams.get("q"); + const [sqlDraft, setSqlDraft] = useState(DEFAULT_SQL_EXAMPLE); + + useEffect(() => { + document.title = "SQL | HyperLEDA"; + }, []); + + useLayoutEffect(() => { + if (!permalinkSql) { + return; + } + setSqlDraft(parseSqlPermalink(permalinkSql)); + }, [permalinkSql]); + + function handleQueryRun(sql: string): void { + setSearchParams({ q: sql }, { replace: true }); + } + + return ( +
+ +
+ ); +} diff --git a/apps/admin/src/pages/catalog/CatalogSqlPanel.tsx b/apps/admin/src/pages/catalog/CatalogSqlPanel.tsx index 1561b28..d22689d 100644 --- a/apps/admin/src/pages/catalog/CatalogSqlPanel.tsx +++ b/apps/admin/src/pages/catalog/CatalogSqlPanel.tsx @@ -1,6 +1,9 @@ import { FormEvent, ReactElement, useEffect, useRef, useState } from "react"; import { useLocation } from "react-router-dom"; -import type { TapSchemaEntry } from "@hyperleda/lib/clients/backend"; +import type { + TapSchemaEntry, + TapSyncResponse, +} from "@hyperleda/lib/clients/backend"; import { Button, Text } from "@hyperleda/lib/ui"; import { SqlEditor } from "./SqlEditor"; import { SqlQueryEmbed } from "@hyperleda/lib/ui"; @@ -18,6 +21,7 @@ interface CatalogSqlPanelProps { schemas?: TapSchemaEntry[]; permalinkRunKey?: string | null; onQueryRun?: (sql: string) => void; + executeQuery?: (sql: string) => Promise; } export function CatalogSqlPanel({ @@ -26,6 +30,7 @@ export function CatalogSqlPanel({ schemas, permalinkRunKey, onQueryRun, + executeQuery, }: CatalogSqlPanelProps): ReactElement { const [executedSql, setExecutedSql] = useState(null); const [runId, setRunId] = useState(0); @@ -112,6 +117,7 @@ export function CatalogSqlPanel({ key={runId} sql={executedSql} onLoadingChange={setLoading} + executeQuery={executeQuery} /> ) : null} diff --git a/apps/admin/src/pages/index.ts b/apps/admin/src/pages/index.ts index 21b25ee..b16979b 100644 --- a/apps/admin/src/pages/index.ts +++ b/apps/admin/src/pages/index.ts @@ -1,3 +1,4 @@ +export { SqlQueryPage } from "./SqlQuery"; export { AdminPage } from "./Admin"; export { AdminMergePgcPage } from "./AdminMergePgc"; export { CrossmatchResultsPage } from "./CrossmatchResults"; diff --git a/packages/lib/src/ui/SqlQueryEmbed.tsx b/packages/lib/src/ui/SqlQueryEmbed.tsx index 36fbecb..d062ab6 100644 --- a/packages/lib/src/ui/SqlQueryEmbed.tsx +++ b/packages/lib/src/ui/SqlQueryEmbed.tsx @@ -26,9 +26,11 @@ function markdownColumns(columns: Column[]): Column[] { export function SqlQueryEmbed({ sql, onLoadingChange, + executeQuery = executeSqlQuery, }: { sql: string; onLoadingChange?: (loading: boolean) => void; + executeQuery?: (sql: string) => Promise; }): ReactElement { const [result, setResult] = useState(null); const [loading, setLoading] = useState(true); @@ -50,7 +52,7 @@ export function SqlQueryEmbed({ setResult(null); try { - const payload = await executeSqlQuery(sql); + const payload = await executeQuery(sql); if (!cancelled) { setResult(payload); } @@ -69,7 +71,7 @@ export function SqlQueryEmbed({ return () => { cancelled = true; }; - }, [sql]); + }, [sql, executeQuery]); const tableData = result ? syncPayloadToTable(result) : null; const rowCount = tableData?.rows.length ?? 0; From 7318f223b8b31f4df24cc3a92819628c71fe8468 Mon Sep 17 00:00:00 2001 From: kraysent Date: Tue, 18 Aug 2026 12:49:15 +0100 Subject: [PATCH 2/2] add /sql page to public interface as well --- apps/admin/src/pages/DataCatalog.tsx | 2 +- apps/admin/src/pages/SqlQuery.tsx | 2 +- apps/web/package.json | 2 ++ apps/web/src/App.tsx | 2 ++ apps/web/src/components/ui/Navbar.tsx | 3 +- apps/web/src/pages/SqlQuery.tsx | 36 +++++++++++++++++++ bun.lock | 4 +++ packages/lib/package.json | 2 ++ .../lib/src/ui}/CatalogSqlPanel.tsx | 5 +-- .../lib/src/ui}/SqlEditor.tsx | 2 +- packages/lib/src/ui/index.ts | 1 + 11 files changed, 55 insertions(+), 6 deletions(-) create mode 100644 apps/web/src/pages/SqlQuery.tsx rename {apps/admin/src/pages/catalog => packages/lib/src/ui}/CatalogSqlPanel.tsx (96%) rename {apps/admin/src/pages/catalog => packages/lib/src/ui}/SqlEditor.tsx (98%) diff --git a/apps/admin/src/pages/DataCatalog.tsx b/apps/admin/src/pages/DataCatalog.tsx index e2108b2..08c9b4a 100644 --- a/apps/admin/src/pages/DataCatalog.tsx +++ b/apps/admin/src/pages/DataCatalog.tsx @@ -32,7 +32,7 @@ import { useDataFetching } from "@hyperleda/lib/hooks"; import { Button } from "@hyperleda/lib/ui"; import classNames from "classnames"; import { CatalogViewTabs } from "./catalog/CatalogViewTabs"; -import { CatalogSqlPanel } from "./catalog/CatalogSqlPanel"; +import { CatalogSqlPanel } from "@hyperleda/lib/ui"; import { DEFAULT_SQL_EXAMPLE, defaultSelectForTable, diff --git a/apps/admin/src/pages/SqlQuery.tsx b/apps/admin/src/pages/SqlQuery.tsx index 9069bf8..26d1903 100644 --- a/apps/admin/src/pages/SqlQuery.tsx +++ b/apps/admin/src/pages/SqlQuery.tsx @@ -8,7 +8,7 @@ import { formatApiError, parseSqlPermalink, } from "@hyperleda/lib/tap"; -import { CatalogSqlPanel } from "./catalog/CatalogSqlPanel"; +import { CatalogSqlPanel } from "@hyperleda/lib/ui"; async function executeAdminSqlQuery(sql: string): Promise { const response = await tapSync({ diff --git a/apps/web/package.json b/apps/web/package.json index c26f450..e8cd595 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -13,11 +13,13 @@ }, "dependencies": { "@hyperleda/lib": "*", + "@monaco-editor/react": "^4.7.0", "@tailwindcss/typography": "^0.5.20", "@tailwindcss/vite": "^4.3.3", "axios": "^1.19.0", "classnames": "^2.5.1", "flowbite-react": "^0.12.17", + "monaco-editor": "^0.56.0", "react": "^19.2.8", "react-dom": "^19.2.8", "react-icons": "^5.7.0", diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index ae9b540..260af1f 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -6,6 +6,7 @@ import { HomePage } from "./pages/Home"; import { SearchResultsPage } from "./pages/SearchResults"; import { ObjectDetailsPage } from "./pages/ObjectDetails"; import { NotFoundPage } from "./pages/NotFound"; +import { SqlQueryPage } from "./pages/SqlQuery"; import { Navbar } from "./components/ui/Navbar"; import { SearchBar } from "./components/ui/Searchbar"; @@ -53,6 +54,7 @@ function App() { } /> } /> + } /> , label: "Object search", end: true }, + { to: "/sql", icon: , label: "SQL", end: true }, ]; const configuredProductionWeb = "https://leda.sao.ru"; diff --git a/apps/web/src/pages/SqlQuery.tsx b/apps/web/src/pages/SqlQuery.tsx new file mode 100644 index 0000000..8b1b279 --- /dev/null +++ b/apps/web/src/pages/SqlQuery.tsx @@ -0,0 +1,36 @@ +import { ReactElement, useEffect, useLayoutEffect, useState } from "react"; +import { useSearchParams } from "react-router-dom"; +import { CatalogSqlPanel } from "@hyperleda/lib/ui"; +import { DEFAULT_SQL_EXAMPLE, parseSqlPermalink } from "@hyperleda/lib/tap"; + +export function SqlQueryPage(): ReactElement { + const [searchParams, setSearchParams] = useSearchParams(); + const permalinkSql = searchParams.get("q"); + const [sqlDraft, setSqlDraft] = useState(DEFAULT_SQL_EXAMPLE); + + useEffect(() => { + document.title = "SQL | HyperLEDA"; + }, []); + + useLayoutEffect(() => { + if (!permalinkSql) { + return; + } + setSqlDraft(parseSqlPermalink(permalinkSql)); + }, [permalinkSql]); + + function handleQueryRun(sql: string): void { + setSearchParams({ q: sql }, { replace: true }); + } + + return ( +
+ +
+ ); +} diff --git a/bun.lock b/bun.lock index f1a52d3..dbf5015 100644 --- a/bun.lock +++ b/bun.lock @@ -47,11 +47,13 @@ "version": "0.0.0", "dependencies": { "@hyperleda/lib": "*", + "@monaco-editor/react": "^4.7.0", "@tailwindcss/typography": "^0.5.20", "@tailwindcss/vite": "^4.3.3", "axios": "^1.19.0", "classnames": "^2.5.1", "flowbite-react": "^0.12.17", + "monaco-editor": "^0.56.0", "react": "^19.2.8", "react-dom": "^19.2.8", "react-icons": "^5.7.0", @@ -84,6 +86,8 @@ "typescript": "^6.0.3", }, "peerDependencies": { + "@monaco-editor/react": "^4.7.0", + "monaco-editor": "^0.56.0", "react": "^19.2.8", "react-dom": "^19.2.8", "react-icons": "^5.7.0", diff --git a/packages/lib/package.json b/packages/lib/package.json index b147335..2fb9d3f 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -21,6 +21,8 @@ "react-markdown": "^10.1.0" }, "peerDependencies": { + "@monaco-editor/react": "^4.7.0", + "monaco-editor": "^0.56.0", "react": "^19.2.8", "react-dom": "^19.2.8", "react-icons": "^5.7.0", diff --git a/apps/admin/src/pages/catalog/CatalogSqlPanel.tsx b/packages/lib/src/ui/CatalogSqlPanel.tsx similarity index 96% rename from apps/admin/src/pages/catalog/CatalogSqlPanel.tsx rename to packages/lib/src/ui/CatalogSqlPanel.tsx index d22689d..f9f8d60 100644 --- a/apps/admin/src/pages/catalog/CatalogSqlPanel.tsx +++ b/packages/lib/src/ui/CatalogSqlPanel.tsx @@ -4,9 +4,10 @@ import type { TapSchemaEntry, TapSyncResponse, } from "@hyperleda/lib/clients/backend"; -import { Button, Text } from "@hyperleda/lib/ui"; +import { Button } from "./Button"; import { SqlEditor } from "./SqlEditor"; -import { SqlQueryEmbed } from "@hyperleda/lib/ui"; +import { SqlQueryEmbed } from "./SqlQueryEmbed"; +import { Text } from "./Text"; function runQueryShortcutLabel(): string { if (typeof navigator === "undefined") { diff --git a/apps/admin/src/pages/catalog/SqlEditor.tsx b/packages/lib/src/ui/SqlEditor.tsx similarity index 98% rename from apps/admin/src/pages/catalog/SqlEditor.tsx rename to packages/lib/src/ui/SqlEditor.tsx index 360e6d5..cf1d996 100644 --- a/apps/admin/src/pages/catalog/SqlEditor.tsx +++ b/packages/lib/src/ui/SqlEditor.tsx @@ -2,7 +2,7 @@ import { ReactElement, useEffect, useMemo, useRef, useState } from "react"; import Editor from "@monaco-editor/react"; import type * as Monaco from "monaco-editor"; import type { TapSchemaEntry } from "@hyperleda/lib/clients/backend"; -import { useTheme } from "@hyperleda/lib/ui"; +import { useTheme } from "./useTheme"; type CatalogCompletionTemplate = Omit; diff --git a/packages/lib/src/ui/index.ts b/packages/lib/src/ui/index.ts index cf37010..937dc5c 100644 --- a/packages/lib/src/ui/index.ts +++ b/packages/lib/src/ui/index.ts @@ -35,3 +35,4 @@ export { Loading } from "./Loading"; export { Text, type TextStyle, type TextSize, type TextType } from "./Text"; export { Markdown } from "./Markdown"; export { SqlQueryEmbed } from "./SqlQueryEmbed"; +export { CatalogSqlPanel } from "./CatalogSqlPanel";