Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion src/components/App.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="app">
<header className="app-header">
Expand Down
31 changes: 30 additions & 1 deletion src/components/QueryInput.tsx
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -15,6 +30,17 @@ export function QueryInput() {
const queryTiming = useStore((s) => s.queryTiming);

const fileInputRef = useRef<HTMLInputElement>(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();
Expand Down Expand Up @@ -94,6 +120,9 @@ export function QueryInput() {
>
Upload
</button>
<button className="query-btn secondary" onClick={handleShare} title="Copy shareable URL to clipboard">
{shareLabel}
</button>
<button className="query-btn secondary" onClick={handleReset} disabled={isLoading}>
Reset
</button>
Expand Down