diff --git a/src/components/catalog/CatalogSqlPanel.tsx b/src/components/catalog/CatalogSqlPanel.tsx index 5813891..72cef77 100644 --- a/src/components/catalog/CatalogSqlPanel.tsx +++ b/src/components/catalog/CatalogSqlPanel.tsx @@ -1,5 +1,5 @@ -import { FormEvent, ReactElement, useState } from "react"; -import { Link as RouterLink } from "react-router-dom"; +import { FormEvent, ReactElement, useEffect, useRef, useState } from "react"; +import { Link as RouterLink, useLocation } from "react-router-dom"; import type { TapSchemaEntry, TapSyncResponse, @@ -23,6 +23,8 @@ interface CatalogSqlPanelProps { onSqlChange: (sql: string) => void; schemas?: TapSchemaEntry[]; loggedIn: boolean; + permalinkRunKey?: string | null; + onQueryRun?: (sql: string) => void; } export function CatalogSqlPanel({ @@ -30,11 +32,15 @@ export function CatalogSqlPanel({ onSqlChange, schemas, loggedIn, + permalinkRunKey, + onQueryRun, }: CatalogSqlPanelProps): ReactElement { const [result, setResult] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const runShortcut = runQueryShortcutLabel(); + const location = useLocation(); + const didAutoRun = useRef(false); async function runQuery(): Promise { if (!loggedIn || loading) { @@ -50,6 +56,7 @@ export function CatalogSqlPanel({ setLoading(true); setError(null); setResult(null); + onQueryRun?.(trimmed); try { const payload = await executeSqlQuery(trimmed); @@ -68,6 +75,21 @@ export function CatalogSqlPanel({ await runQuery(); } + useEffect(() => { + didAutoRun.current = false; + }, [location.key]); + + useEffect(() => { + if (!permalinkRunKey || !loggedIn || didAutoRun.current) { + return; + } + if (sql.trim() !== permalinkRunKey.trim()) { + return; + } + didAutoRun.current = true; + void runQuery(); + }, [permalinkRunKey, loggedIn, sql]); + const tableData = result ? syncPayloadToTable(result) : null; const rowCount = tableData?.rows.length ?? 0; diff --git a/src/lib/tap.ts b/src/lib/tap.ts index 0c35340..c5f990b 100644 --- a/src/lib/tap.ts +++ b/src/lib/tap.ts @@ -61,3 +61,14 @@ export function syncPayloadToTable(payload: TapSyncResponse): { export function defaultSelectForTable(tableName: string, limit = 25): string { return `SELECT * FROM ${tableName} LIMIT ${limit}`; } + +export function parseSqlPermalink(raw: string): string { + const trimmed = raw.trim(); + if ( + (trimmed.startsWith('"') && trimmed.endsWith('"')) || + (trimmed.startsWith("'") && trimmed.endsWith("'")) + ) { + return trimmed.slice(1, -1); + } + return trimmed; +} diff --git a/src/pages/DataCatalog.tsx b/src/pages/DataCatalog.tsx index 6170163..42a5fd7 100644 --- a/src/pages/DataCatalog.tsx +++ b/src/pages/DataCatalog.tsx @@ -1,5 +1,16 @@ -import { ReactElement, useEffect, useMemo, useState } from "react"; -import { useMatch, useNavigate, useParams } from "react-router-dom"; +import { + ReactElement, + useEffect, + useLayoutEffect, + useMemo, + useState, +} from "react"; +import { + useMatch, + useNavigate, + useParams, + useSearchParams, +} from "react-router-dom"; import { tapSync, tapTables } from "../clients/backend/sdk.gen"; import type { ListTapTablesResponse, @@ -30,6 +41,7 @@ import { DEFAULT_SQL_EXAMPLE, defaultSelectForTable, formatApiError, + parseSqlPermalink, } from "../lib/tap"; async function fetchTablesList(): Promise { @@ -309,7 +321,9 @@ export function DataCatalogPage(): ReactElement { tableName?: string; }>(); const navigate = useNavigate(); + const [searchParams, setSearchParams] = useSearchParams(); const isQueryMode = Boolean(useMatch("/data-catalog/query")); + const permalinkSql = searchParams.get("q"); const [filter, setFilter] = useState(""); const [sqlDraft, setSqlDraft] = useState(DEFAULT_SQL_EXAMPLE); const loggedIn = isLoggedIn(); @@ -332,6 +346,13 @@ export function DataCatalogPage(): ReactElement { : "Data catalog | HyperLEDA"; }, [isQueryMode]); + useLayoutEffect(() => { + if (!isQueryMode || !permalinkSql) { + return; + } + setSqlDraft(parseSqlPermalink(permalinkSql)); + }, [isQueryMode, permalinkSql]); + const { data: tablesPayload, loading: tablesLoading, @@ -364,8 +385,17 @@ export function DataCatalogPage(): ReactElement { function openSqlEditor(sql?: string): void { if (sql) { setSqlDraft(sql); + navigate({ + pathname: "/data-catalog/query", + search: `?q=${encodeURIComponent(sql)}`, + }); + return; } - navigate("/data-catalog/query"); + navigate({ pathname: "/data-catalog/query", search: "" }); + } + + function handleQueryRun(sql: string): void { + setSearchParams({ q: sql }, { replace: true }); } function handleSelect(nextSchema: string, nextTable: string): void { @@ -413,6 +443,10 @@ export function DataCatalogPage(): ReactElement { onSqlChange={setSqlDraft} schemas={tablesPayload?.schemas} loggedIn={loggedIn} + permalinkRunKey={ + permalinkSql ? parseSqlPermalink(permalinkSql) : null + } + onQueryRun={handleQueryRun} /> ); }