Skip to content
Draft
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
928 changes: 0 additions & 928 deletions bun.lock

This file was deleted.

4 changes: 4 additions & 0 deletions knip.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"$schema": "https://unpkg.com/knip@latest/schema.json",
"ignore": ["src/components/ui/**"]
}
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@
},
"dependencies": {
"@duckdb/duckdb-wasm": "1.29.1-dev24.0",
"@radix-ui/react-checkbox": "^1.1.4",
"@radix-ui/react-dropdown-menu": "^2.1.5",
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-toast": "^1.2.5",
"@tanstack/react-table": "^8.20.6",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.474.0",
"date-fns": "^4.1.0",
"lucide-react": "^0.475.0",
"monaco-editor": "^0.52.2",
"papaparse": "^5.5.1",
"react": "^19.0.0",
Expand Down
7 changes: 6 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,12 @@ function App() {
const logger = new duckdb.ConsoleLogger();
const dbInstance = new duckdb.AsyncDuckDB(logger, worker);
await dbInstance.instantiate(bundle.mainModule, bundle.pthreadWorker);
// TODO: load opfs file
const now = new Date();
const timestamp = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}-${String(now.getHours()).padStart(2, '0')}-${String(now.getMinutes()).padStart(2, '0')}-${String(now.getSeconds()).padStart(2, '0')}`;
await dbInstance.open({
path: `opfs://${timestamp}.db`,
accessMode: duckdb.DuckDBAccessMode.READ_WRITE,
})
URL.revokeObjectURL(worker_url);
setDb(dbInstance);
}
Expand Down
71 changes: 66 additions & 5 deletions src/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,21 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Button } from "./components/ui/button";
import { Undo2, Redo2, Download } from "lucide-react";
import { format } from "date-fns";
import { useToast } from "@/hooks/use-toast";

interface EditorProps {
editorRef: React.MutableRefObject<monaco.editor.IStandaloneCodeEditor | null>;
editorRef: React.RefObject<monaco.editor.IStandaloneCodeEditor | null>;
runQuery: () => void;
}

const Editor: React.FC<EditorProps> = ({ editorRef, runQuery }) => {
const defaultFontSize = 20;
const [fontSize, setFontSize] = useState(defaultFontSize);
const [theme, setTheme] = useState("vs-dark");
const { toast } = useToast();

useEffect(() => {
if (editorRef.current) {
Expand Down Expand Up @@ -79,12 +84,68 @@ const Editor: React.FC<EditorProps> = ({ editorRef, runQuery }) => {
style={{ display: "flex", alignItems: "center", marginBottom: "0px" }}
>
<span>Font Size: {fontSize}</span>
<button onClick={increaseFontSize} style={{ marginLeft: "8px" }}>
<Button onClick={increaseFontSize} style={{ marginLeft: "8px" }}>
</button>
<button onClick={decreaseFontSize} style={{ marginLeft: "4px" }}>
</Button>
<Button onClick={decreaseFontSize} style={{ marginLeft: "4px" }}>
</button>
</Button>
<Button
onClick={() => {
if (editorRef.current) {
const content = editorRef.current.getValue();
const timestamp = format(new Date(), "yyyy-MM-dd'T'HHmmssSSS");
const filename = `${timestamp}.sql`;

const blob = new Blob([content], { type: "text/plain" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);

toast({
title: "Downloaded",
description: `File saved as ${filename}`,
});
}
}}
size="icon"
variant="outline"
title="Download SQL"
className="ml-2"
>
<Download className="h-4 w-4" />
</Button>
<Button
onClick={() => {
if (editorRef.current) {
editorRef.current.trigger("keyboard", "undo", null);
}
}}
size="icon"
variant="outline"
title="Undo"
className="ml-2"
>
<Undo2 className="h-4 w-4" />
</Button>
<Button
onClick={() => {
if (editorRef.current) {
editorRef.current.trigger("keyboard", "redo", null);
}
}}
size="icon"
variant="outline"
title="Redo"
className="ml-2"
>
<Redo2 className="h-4 w-4" />
</Button>
<Select onValueChange={handleThemeChange} value={theme}>
<SelectTrigger className="w-[180px] ml-2">
<SelectValue placeholder="Select theme" />
Expand Down
4 changes: 2 additions & 2 deletions src/components/InputSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ const InputSection: React.FC<InputSectionProps> = ({
>
<div style={{ display: "flex", alignItems: "center", gap: "10px" }}>
<h3>Upload CSV Preview</h3>
<button
<Button
onClick={createTable}
style={{
marginTop: "10px",
Expand All @@ -188,7 +188,7 @@ const InputSection: React.FC<InputSectionProps> = ({
}}
>
Confirm table name, column types and Create Table
</button>
</Button>
</div>
<input
type="text"
Expand Down
129 changes: 104 additions & 25 deletions src/components/OPFSViewer.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import React, { useState, useEffect, useCallback } from 'react';
import { Checkbox } from "@/components/ui/checkbox";
import { Button } from './ui/button';
import { Download, Trash2 } from 'lucide-react';

type FileSystemEntry = {
name: string;
Expand All @@ -8,7 +11,8 @@ type FileSystemEntry = {

async function readDirectory(
dirHandle: FileSystemDirectoryHandle,
currentName = '(root)'
currentName = '(root)',
hideWalFile = true
): Promise<FileSystemEntry> {
const entry: FileSystemEntry = {
name: currentName,
Expand All @@ -20,10 +24,12 @@ async function readDirectory(
const subEntry = await readDirectory(handle, name);
entry.children?.push(subEntry);
} else if (handle.kind === 'file') {
entry.children?.push({
name,
kind: 'file',
});
if (!hideWalFile || !name.endsWith('.wal')) {
entry.children?.push({
name,
kind: 'file',
});
}
}
}
return entry;
Expand All @@ -38,6 +44,27 @@ async function readFile(
return fileData.text();
}

async function downloadFile(
dirHandle: FileSystemDirectoryHandle,
fileName: string
) {
try {
const content = await readFile(dirHandle, fileName);
const blob = new Blob([content], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = fileName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
} catch (error) {
console.error('Error downloading file:', error);
throw error;
}
}

async function writeFile(
dirHandle: FileSystemDirectoryHandle,
fileName: string,
Expand Down Expand Up @@ -67,12 +94,12 @@ async function deleteAll(dirHandle: FileSystemDirectoryHandle) {
}
}


const FileSystemTree: React.FC<{
entry: FileSystemEntry;
onClickFile: (fileName: string) => void;
onDelete: (entryName: string, isDir: boolean) => void;
}> = ({ entry, onClickFile, onDelete }) => {
onDownload: (fileName: string) => void;
}> = ({ entry, onClickFile, onDelete, onDownload }) => {
if (!entry) return null;

return (
Expand All @@ -81,12 +108,15 @@ const FileSystemTree: React.FC<{
{entry.kind === 'directory' ? '📁' : '📄'}{' '}
<strong>{entry.name}</strong>{' '}
{entry.name !== '(root)' && (
<button
style={{ marginLeft: '8px', color: 'red' }}
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-red-500 hover:text-red-700"
onClick={() => onDelete(entry.name, entry.kind === 'directory')}
title="Delete directory"
>
Delete
</button>
<Trash2 className="h-4 w-4" />
</Button>
)}
</li>
{entry.children &&
Expand All @@ -104,15 +134,30 @@ const FileSystemTree: React.FC<{
entry={child}
onClickFile={onClickFile}
onDelete={onDelete}
onDownload={onDownload}
/>
)}
{child.kind === 'file' && (
<button
style={{ marginLeft: '8px', color: 'red' }}
onClick={() => onDelete(child.name, false)}
>
Delete
</button>
<>
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={() => onDownload(child.name)}
title="Download file"
>
<Download className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-red-500 hover:text-red-700"
onClick={() => onDelete(child.name, false)}
title="Delete file"
>
<Trash2 className="h-4 w-4" />
</Button>
</>
)}
</li>
))}
Expand All @@ -121,6 +166,7 @@ const FileSystemTree: React.FC<{
};

const OPFSViewer: React.FC = () => {
const [hideWalFile, setHideWalFile] = useState(true);
const [rootHandle, setRootHandle] = useState<FileSystemDirectoryHandle | null>(
null
);
Expand All @@ -138,7 +184,7 @@ const OPFSViewer: React.FC = () => {
await dirHandle.requestPermission({ mode: 'readwrite' });
setRootHandle(dirHandle);

const treeData = await readDirectory(dirHandle);
const treeData = await readDirectory(dirHandle, '(root)', hideWalFile);
setTree(treeData);
} catch (err) {
console.error(err);
Expand All @@ -148,9 +194,9 @@ const OPFSViewer: React.FC = () => {

const reloadTree = useCallback(async () => {
if (!rootHandle) return;
const treeData = await readDirectory(rootHandle);
const treeData = await readDirectory(rootHandle, '(root)', hideWalFile);
setTree(treeData);
}, [rootHandle]);
}, [rootHandle, hideWalFile]);

const handleClickFile = useCallback(
async (fileName: string) => {
Expand Down Expand Up @@ -186,6 +232,19 @@ const OPFSViewer: React.FC = () => {
}
}, [rootHandle, newFileName, newFileContent, reloadTree]);

const handleDownload = useCallback(
async (fileName: string) => {
if (!rootHandle) return;
try {
await downloadFile(rootHandle, fileName);
} catch (err) {
console.error(err);
setError(`download error: ${fileName}`);
}
},
[rootHandle]
);

const handleDelete = useCallback(
async (entryName: string, isDir: boolean) => {
if (!rootHandle) return;
Expand Down Expand Up @@ -240,16 +299,35 @@ const OPFSViewer: React.FC = () => {
loadOPFS();
}, [loadOPFS]);

useEffect(() => {
reloadTree();
}, [hideWalFile, reloadTree]);

return (
<div style={{ padding: '1rem' }}>
<h1>OPFS Viewer</h1>
<div className="flex items-center gap-4">
<h1>OPFS Viewer</h1>
<div className="flex items-center space-x-2">
<Checkbox
id="hide-wal"
checked={hideWalFile}
onCheckedChange={(checked) => setHideWalFile(checked === true)}
/>
<label
htmlFor="hide-wal"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Hide WAL Files
</label>
</div>
</div>
{error && <p style={{ color: 'red' }}>{error}</p>}

<div style={{ margin: '1rem 0' }}>
<button onClick={reloadTree}>Reload Tree</button>
<button style={{ marginLeft: 8 }} onClick={handleDeleteAll}>
<Button onClick={reloadTree}>Reload Tree</Button>
<Button style={{ marginLeft: 8 }} onClick={handleDeleteAll}>
DELETE ALL
</button>
</Button>
</div>

{tree && (
Expand All @@ -258,6 +336,7 @@ const OPFSViewer: React.FC = () => {
entry={tree}
onClickFile={handleClickFile}
onDelete={handleDelete}
onDownload={handleDownload}
/>
</div>
)}
Expand Down Expand Up @@ -297,7 +376,7 @@ const OPFSViewer: React.FC = () => {
/>
</div>
<div style={{ marginTop: '8px' }}>
<button onClick={handleWriteFile}>Save File</button>
<Button onClick={handleWriteFile}>Save File</Button>
</div>
<hr />
<h2>Upload CSV</h2>
Expand Down
Loading
Loading