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/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/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 90% rename from apps/admin/src/pages/catalog/CatalogSqlPanel.tsx rename to packages/lib/src/ui/CatalogSqlPanel.tsx index 1561b28..f9f8d60 100644 --- a/apps/admin/src/pages/catalog/CatalogSqlPanel.tsx +++ b/packages/lib/src/ui/CatalogSqlPanel.tsx @@ -1,9 +1,13 @@ import { FormEvent, ReactElement, useEffect, useRef, useState } from "react"; import { useLocation } from "react-router-dom"; -import type { TapSchemaEntry } from "@hyperleda/lib/clients/backend"; -import { Button, Text } from "@hyperleda/lib/ui"; +import type { + TapSchemaEntry, + TapSyncResponse, +} from "@hyperleda/lib/clients/backend"; +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") { @@ -18,6 +22,7 @@ interface CatalogSqlPanelProps { schemas?: TapSchemaEntry[]; permalinkRunKey?: string | null; onQueryRun?: (sql: string) => void; + executeQuery?: (sql: string) => Promise; } export function CatalogSqlPanel({ @@ -26,6 +31,7 @@ export function CatalogSqlPanel({ schemas, permalinkRunKey, onQueryRun, + executeQuery, }: CatalogSqlPanelProps): ReactElement { const [executedSql, setExecutedSql] = useState(null); const [runId, setRunId] = useState(0); @@ -112,6 +118,7 @@ export function CatalogSqlPanel({ key={runId} sql={executedSql} onLoadingChange={setLoading} + executeQuery={executeQuery} /> ) : null} 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/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; 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";