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";