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
+