diff --git a/src/components/App.tsx b/src/components/App.tsx index 3f5a267..d099858 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -1,11 +1,38 @@ +import { useEffect } from 'react'; import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels'; import { HexViewer } from './HexViewer/HexViewer'; import { AstTree } from './AstTree/AstTree'; -import { QueryInput } from './QueryInput'; +import { QueryInput, decodeBase64Url } from './QueryInput'; +import { useStore } from '../store/store'; +import { ClickHouseFormat } from '../core/types/formats'; import logo from '../assets/clickhouse-yellow-badge.svg'; import '../styles/app.css'; function App() { + const setQuery = useStore((s) => s.setQuery); + const setFormat = useStore((s) => s.setFormat); + + useEffect(() => { + const params = new URLSearchParams(window.location.search); + const q = params.get('q'); + const f = params.get('f'); + + if (q) { + try { + setQuery(decodeBase64Url(q)); + } catch { + // ignore malformed base64 + } + } + if (f && Object.values(ClickHouseFormat).includes(f as ClickHouseFormat)) { + setFormat(f as ClickHouseFormat); + } + + if (q || f) { + window.history.replaceState({}, '', window.location.pathname); + } + }, []); // eslint-disable-line react-hooks/exhaustive-deps + return (
diff --git a/src/components/QueryInput.tsx b/src/components/QueryInput.tsx index 9ea04da..5c6d1e9 100644 --- a/src/components/QueryInput.tsx +++ b/src/components/QueryInput.tsx @@ -1,8 +1,23 @@ -import { useCallback, useRef } from 'react'; +import { useCallback, useRef, useState } from 'react'; import { useStore } from '../store/store'; import { DEFAULT_QUERY } from '../core/clickhouse/client'; import { ClickHouseFormat, FORMAT_METADATA } from '../core/types/formats'; +function encodeBase64Url(str: string): string { + const bytes = new TextEncoder().encode(str); + const binary = String.fromCharCode(...bytes); + return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); +} + +function decodeBase64Url(encoded: string): string { + const base64 = encoded.replace(/-/g, '+').replace(/_/g, '/'); + const binary = atob(base64); + const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0)); + return new TextDecoder().decode(bytes); +} + +export { encodeBase64Url, decodeBase64Url }; + export function QueryInput() { const query = useStore((s) => s.query); const setQuery = useStore((s) => s.setQuery); @@ -15,6 +30,17 @@ export function QueryInput() { const queryTiming = useStore((s) => s.queryTiming); const fileInputRef = useRef(null); + const [shareLabel, setShareLabel] = useState('Share'); + + const handleShare = useCallback(() => { + const url = new URL(window.location.href); + url.search = ''; + url.searchParams.set('q', encodeBase64Url(query)); + url.searchParams.set('f', format); + navigator.clipboard.writeText(url.toString()); + setShareLabel('Copied!'); + setTimeout(() => setShareLabel('Share'), 2000); + }, [query, format]); const handleExecute = useCallback(() => { executeQuery(); @@ -94,6 +120,9 @@ export function QueryInput() { > Upload +