feat: support drag-and-drop preview for Parquet, CSV, TSV, JSON files (#35)

Uses DuckDB in-memory connection with read_parquet()/read_csv()/read_json().
No sidebar connection created, tab title shows file name only,
connection/database toolbar hidden for preview tabs.
This commit is contained in:
skyler
2026-04-30 17:49:40 +08:00
committed by GitHub
parent a248615819
commit 7e858296cd
2 changed files with 52 additions and 3 deletions
+43 -3
View File
@@ -190,7 +190,13 @@ function databaseDisplayName(database: string): string {
return database || t("editor.noDatabase");
}
function isPreviewTab(tab: typeof queryStore.tabs[number]): boolean {
const config = connectionStore.getConfig(tab.connectionId);
return !!config?.name.startsWith("[Preview]");
}
function tabDisplayTitle(tab: typeof queryStore.tabs[number]): string {
if (isPreviewTab(tab)) return tab.title;
const database = databaseDisplayNameForTab(tab.connectionId, tab.database);
if (tab.mode === "data" && tab.tableMeta?.tableName) {
return `${database} | ${tab.tableMeta.tableName}`;
@@ -555,14 +561,48 @@ function getDbType(path: string): "sqlite" | "duckdb" | null {
return null;
}
function getDataFileQuery(path: string): string | null {
const lower = path.toLowerCase();
const escaped = path.replace(/'/g, "''");
if (lower.endsWith(".parquet")) return `SELECT * FROM read_parquet('${escaped}') LIMIT 1000`;
if (lower.endsWith(".csv")) return `SELECT * FROM read_csv('${escaped}') LIMIT 1000`;
if (lower.endsWith(".tsv")) return `SELECT * FROM read_csv('${escaped}', delim='\t') LIMIT 1000`;
if (lower.endsWith(".json")) return `SELECT * FROM read_json('${escaped}') LIMIT 1000`;
return null;
}
async function setupFileDrop() {
const webview = getCurrentWebview();
await webview.onDragDropEvent((event) => {
await webview.onDragDropEvent(async (event) => {
if (event.payload.type !== "drop") return;
for (const path of event.payload.paths) {
const name = path.split("/").pop()?.split("\\").pop() || path;
const dataQuery = getDataFileQuery(path);
if (dataQuery) {
const config: ConnectionConfig = {
id: crypto.randomUUID(),
name: `[Preview] ${name}`,
db_type: "duckdb",
driver_profile: "duckdb",
driver_label: "DuckDB",
url_params: "",
host: ":memory:",
port: 0,
username: "",
password: "",
};
const connectionId = await api.connectDb(config);
connectionStore.addEphemeralConnection({ ...config, id: connectionId });
const tabId = queryStore.createTab(connectionId, "", name, "query");
queryStore.updateSql(tabId, dataQuery);
queryStore.executeCurrentTab();
toast(t("welcome.fileOpened", { name }));
continue;
}
const dbType = getDbType(path);
if (!dbType) continue;
const name = path.split("/").pop()?.split("\\").pop() || path;
const config: ConnectionConfig = {
id: crypto.randomUUID(),
name,
@@ -784,7 +824,7 @@ async function setupFileDrop() {
<!-- Editor Panel -->
<div v-if="activeTab" class="flex flex-col flex-1 min-h-0">
<div v-if="activeTab.mode === 'query'" class="h-8 shrink-0 border-b bg-background/80 px-3 flex items-center gap-2 text-xs text-muted-foreground">
<div v-if="activeTab.mode === 'query' && !isPreviewTab(activeTab)" class="h-8 shrink-0 border-b bg-background/80 px-3 flex items-center gap-2 text-xs text-muted-foreground">
<Server class="h-3.5 w-3.5 shrink-0" />
<span v-if="activeConnection?.color" class="h-4 w-1 rounded-full shrink-0" :style="{ backgroundColor: activeConnection.color }" />
<Select
+9
View File
@@ -597,6 +597,14 @@ export const useConnectionStore = defineStore("connection", () => {
}));
}
function addEphemeralConnection(config: ConnectionConfig) {
const normalized = normalizeConnection(config);
if (!connections.value.find((c) => c.id === normalized.id)) {
connections.value.push(normalized);
}
connectedIds.value.add(normalized.id);
}
return {
connections,
activeConnectionId,
@@ -606,6 +614,7 @@ export const useConnectionStore = defineStore("connection", () => {
isTreeNodePinned,
toggleTreeNodePin,
addConnection,
addEphemeralConnection,
updateConnection,
removeConnection,
editingConnectionId,