mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-29 16:18:55 +00:00
feat: add web deployment with axum server, Docker support, and browser UI
Introduce src-web (axum HTTP server) exposing all 43 database operations as REST APIs with SSE streaming for AI, transfers, and SQL file execution. Frontend adapts automatically between Tauri and HTTP backends via api.ts. Includes password authentication, Docker packaging, and login page.
This commit is contained in:
+58
-21
@@ -51,10 +51,8 @@ import { useHistoryStore } from "@/stores/historyStore";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import { setLocale, currentLocale, type Locale } from "@/i18n";
|
||||
import { getCurrentWindow, type Theme } from "@tauri-apps/api/window";
|
||||
import { getCurrentWebview } from "@tauri-apps/api/webview";
|
||||
import { getVersion } from "@tauri-apps/api/app";
|
||||
import * as api from "@/lib/tauri";
|
||||
import type { Theme } from "@tauri-apps/api/window";
|
||||
import * as api from "@/lib/api";
|
||||
import { canCancelQueryExecution, queryExecutionLabelKey } from "@/lib/queryExecutionState";
|
||||
import { connectionDriverLabel, connectionIconType, connectionOptionSubtitle } from "@/lib/connectionPresentation";
|
||||
import { resolveExecutableSql } from "@/lib/sqlExecutionTarget";
|
||||
@@ -62,6 +60,7 @@ import { buildTableSelectSql, quoteTableIdentifier } from "@/lib/tableSelectSql"
|
||||
import { isTauriRuntime } from "@/lib/tauriRuntime";
|
||||
import type { SqlFormatDialect } from "@/lib/sqlFormatter";
|
||||
import { isCloseTabShortcut, isExecuteSqlShortcut } from "@/lib/keyboardShortcuts";
|
||||
import LoginPage from "@/components/auth/LoginPage.vue";
|
||||
|
||||
const { t } = useI18n();
|
||||
const connectionStore = useConnectionStore();
|
||||
@@ -70,6 +69,10 @@ const historyStore = useHistoryStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
const { message: toastMessage, visible: toastVisible, toast } = useToast();
|
||||
|
||||
const isDesktop = isTauriRuntime();
|
||||
const needsAuth = ref(!isDesktop);
|
||||
const authenticated = ref(isDesktop);
|
||||
|
||||
const showConnectionDialog = ref(false);
|
||||
const showSettingsDialog = ref(false);
|
||||
const showHistory = ref(false);
|
||||
@@ -869,9 +872,11 @@ const isDark = ref(localStorage.getItem("dbx-theme") === "dark");
|
||||
function applyTheme() {
|
||||
document.documentElement.classList.toggle("dark", isDark.value);
|
||||
if (!isTauriRuntime()) return;
|
||||
getCurrentWindow()
|
||||
.setTheme(isDark.value ? "dark" as Theme : "light" as Theme)
|
||||
.catch(() => {});
|
||||
import("@tauri-apps/api/window").then(({ getCurrentWindow }) => {
|
||||
getCurrentWindow()
|
||||
.setTheme(isDark.value ? "dark" as Theme : "light" as Theme)
|
||||
.catch(() => {});
|
||||
});
|
||||
}
|
||||
|
||||
function toggleTheme() {
|
||||
@@ -880,14 +885,20 @@ function toggleTheme() {
|
||||
applyTheme();
|
||||
}
|
||||
|
||||
import { open } from "@tauri-apps/plugin-shell";
|
||||
function openUrl(url: string) {
|
||||
if (isTauriRuntime()) {
|
||||
import("@tauri-apps/plugin-shell").then(({ open }) => open(url));
|
||||
} else {
|
||||
window.open(url, "_blank");
|
||||
}
|
||||
}
|
||||
|
||||
function openGitHub() {
|
||||
open("https://github.com/t8y2/dbx");
|
||||
openUrl("https://github.com/t8y2/dbx");
|
||||
}
|
||||
|
||||
function openMcpGuide() {
|
||||
open("https://github.com/t8y2/dbx/blob/main/docs/mcp-guide.md");
|
||||
openUrl("https://github.com/t8y2/dbx/blob/main/docs/mcp-guide.md");
|
||||
}
|
||||
|
||||
async function checkUpdates(options: { silent?: boolean } = {}) {
|
||||
@@ -923,7 +934,7 @@ function formatUpdateError(message: string): string {
|
||||
|
||||
function openLatestRelease() {
|
||||
const url = updateInfo.value?.release_url || latestReleaseUrl;
|
||||
open(url);
|
||||
openUrl(url);
|
||||
}
|
||||
|
||||
const isDownloadingUpdate = ref(false);
|
||||
@@ -987,19 +998,44 @@ function handleKeydown(e: KeyboardEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
applyTheme();
|
||||
function initApp() {
|
||||
connectionStore.initFromDisk().catch((e: any) => {
|
||||
toast(t("connection.loadFailed", { message: e?.message || String(e) }), 5000);
|
||||
});
|
||||
settingsStore.initAiConfig();
|
||||
}
|
||||
|
||||
function onAuthenticated() {
|
||||
authenticated.value = true;
|
||||
initApp();
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
applyTheme();
|
||||
window.addEventListener("keydown", handleKeydown, true);
|
||||
window.addEventListener("resize", updateScrollButtons);
|
||||
if (isTauriRuntime()) {
|
||||
setupFileDrop().catch(() => {});
|
||||
checkUpdates({ silent: true });
|
||||
|
||||
if (!isDesktop) {
|
||||
try {
|
||||
const res = await fetch("/api/auth/check");
|
||||
const data = await res.json();
|
||||
needsAuth.value = data.required;
|
||||
authenticated.value = data.authenticated;
|
||||
} catch { /* server unreachable */ }
|
||||
if (!needsAuth.value || authenticated.value) {
|
||||
initApp();
|
||||
}
|
||||
api.checkForUpdates().then((info) => { appVersion.value = info.current_version; }).catch(() => {});
|
||||
return;
|
||||
}
|
||||
|
||||
initApp();
|
||||
setupFileDrop().catch(() => {});
|
||||
checkUpdates({ silent: true });
|
||||
import("@tauri-apps/api/app").then(({ getVersion }) => {
|
||||
getVersion().then((v) => { appVersion.value = v; }).catch(() => {});
|
||||
import("@tauri-apps/api/event").then(({ listen }) => {
|
||||
}).catch(() => {});
|
||||
import("@tauri-apps/api/event").then(({ listen }) => {
|
||||
listen<{ connection_id: string; database: string; schema?: string; table: string }>("mcp-open-table", async (event) => {
|
||||
const { connection_id, database, schema, table } = event.payload;
|
||||
|
||||
@@ -1019,7 +1055,7 @@ onMounted(() => {
|
||||
openLineageTarget({ connectionId: connection_id, database, schema, tableName: table });
|
||||
}
|
||||
|
||||
getCurrentWindow().setFocus().catch(() => {});
|
||||
import("@tauri-apps/api/window").then(({ getCurrentWindow }) => getCurrentWindow().setFocus().catch(() => {}));
|
||||
});
|
||||
listen<{ connection_id: string; database: string; sql: string }>("mcp-execute-query", async (event) => {
|
||||
const { connection_id, database, sql } = event.payload;
|
||||
@@ -1033,10 +1069,9 @@ onMounted(() => {
|
||||
const tabId = queryStore.createTab(connection_id, database, undefined, "query");
|
||||
queryStore.updateSql(tabId, sql);
|
||||
await queryStore.executeTabSql(tabId, sql);
|
||||
getCurrentWindow().setFocus().catch(() => {});
|
||||
import("@tauri-apps/api/window").then(({ getCurrentWindow }) => getCurrentWindow().setFocus().catch(() => {}));
|
||||
});
|
||||
}).catch(() => {});
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
@@ -1064,6 +1099,7 @@ function getDataFileQuery(path: string): string | null {
|
||||
}
|
||||
|
||||
async function setupFileDrop() {
|
||||
const { getCurrentWebview } = await import("@tauri-apps/api/webview");
|
||||
const webview = getCurrentWebview();
|
||||
await webview.onDragDropEvent(async (event) => {
|
||||
if (event.payload.type !== "drop") return;
|
||||
@@ -1120,7 +1156,8 @@ async function setupFileDrop() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TooltipProvider :delay-duration="300">
|
||||
<LoginPage v-if="needsAuth && !authenticated" @authenticated="onAuthenticated" />
|
||||
<TooltipProvider v-show="!needsAuth || authenticated" :delay-duration="300">
|
||||
<div class="h-screen w-screen flex flex-col bg-background text-foreground overflow-hidden">
|
||||
<!-- Toolbar -->
|
||||
<div class="h-10 flex items-center gap-1 px-2 border-b bg-muted/30 shrink-0">
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { DatabaseZap } from "lucide-vue-next";
|
||||
|
||||
const emit = defineEmits<{ authenticated: [] }>();
|
||||
|
||||
const password = ref("");
|
||||
const error = ref("");
|
||||
const loading = ref(false);
|
||||
|
||||
async function login() {
|
||||
loading.value = true;
|
||||
error.value = "";
|
||||
try {
|
||||
const res = await fetch("/api/auth/login", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ password: password.value }),
|
||||
});
|
||||
if (res.ok) {
|
||||
emit("authenticated");
|
||||
} else {
|
||||
const text = await res.text();
|
||||
error.value = text || "密码错误";
|
||||
}
|
||||
} catch (e: any) {
|
||||
error.value = e?.message || "连接失败";
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center justify-center h-screen bg-background">
|
||||
<div class="w-80 space-y-6 text-center">
|
||||
<div class="flex items-center justify-center gap-2 text-2xl font-bold">
|
||||
<DatabaseZap class="w-8 h-8" />
|
||||
<span>DBX</span>
|
||||
</div>
|
||||
<form class="space-y-4" @submit.prevent="login">
|
||||
<Input
|
||||
v-model="password"
|
||||
type="password"
|
||||
placeholder="请输入访问密码"
|
||||
autofocus
|
||||
/>
|
||||
<p v-if="error" class="text-sm text-destructive">{{ error }}</p>
|
||||
<Button type="submit" class="w-full" :disabled="loading || !password">
|
||||
{{ loading ? "登录中..." : "登录" }}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -14,8 +14,8 @@ import type { ConnectionConfig, DatabaseType } from "@/types/database";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
|
||||
import * as api from "@/lib/tauri";
|
||||
import { open as openFileDialog } from "@tauri-apps/plugin-dialog";
|
||||
import * as api from "@/lib/api";
|
||||
import { isTauriRuntime } from "@/lib/tauriRuntime";
|
||||
import { ArrowLeft, ChevronRight, Copy, FolderOpen, Grid3X3, List, Search } from "lucide-vue-next";
|
||||
|
||||
type DbOption = { value: string; label: string };
|
||||
@@ -402,12 +402,15 @@ watch([() => editingId.value, () => open.value], () => {
|
||||
});
|
||||
|
||||
async function browseSshKeyPath() {
|
||||
const selected = await openFileDialog({
|
||||
title: "Select SSH Private Key",
|
||||
multiple: false,
|
||||
});
|
||||
if (selected && typeof selected === "string") {
|
||||
form.value.ssh_key_path = selected;
|
||||
if (isTauriRuntime()) {
|
||||
const { open } = await import("@tauri-apps/plugin-dialog");
|
||||
const selected = await open({
|
||||
title: "Select SSH Private Key",
|
||||
multiple: false,
|
||||
});
|
||||
if (selected && typeof selected === "string") {
|
||||
form.value.ssh_key_path = selected;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
|
||||
import * as api from "@/lib/tauri";
|
||||
import * as api from "@/lib/api";
|
||||
import type { DatabaseType } from "@/types/database";
|
||||
import {
|
||||
buildDiagramRelationships,
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
ZoomIn, ZoomOut,
|
||||
} from "lucide-vue-next";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import { isTauriRuntime } from "@/lib/tauriRuntime";
|
||||
|
||||
const { t } = useI18n();
|
||||
const { toast } = useToast();
|
||||
@@ -571,10 +572,6 @@ function currentDiagramSvg(): string {
|
||||
|
||||
async function exportSvg() {
|
||||
try {
|
||||
const [{ save }, { writeTextFile }] = await Promise.all([
|
||||
import("@tauri-apps/plugin-dialog"),
|
||||
import("@tauri-apps/plugin-fs"),
|
||||
]);
|
||||
const scopeName = isSchemaAware.value && schema.value
|
||||
? `${database.value}-${schema.value}`
|
||||
: database.value;
|
||||
@@ -583,13 +580,28 @@ async function exportSvg() {
|
||||
scopeName,
|
||||
diagramMode.value,
|
||||
);
|
||||
const path = await save({
|
||||
defaultPath,
|
||||
filters: [{ name: "SVG", extensions: ["svg"] }],
|
||||
});
|
||||
if (!path) return;
|
||||
const svgContent = currentDiagramSvg();
|
||||
|
||||
await writeTextFile(path, currentDiagramSvg());
|
||||
if (isTauriRuntime()) {
|
||||
const [{ save }, { writeTextFile }] = await Promise.all([
|
||||
import("@tauri-apps/plugin-dialog"),
|
||||
import("@tauri-apps/plugin-fs"),
|
||||
]);
|
||||
const path = await save({
|
||||
defaultPath,
|
||||
filters: [{ name: "SVG", extensions: ["svg"] }],
|
||||
});
|
||||
if (!path) return;
|
||||
await writeTextFile(path, svgContent);
|
||||
} else {
|
||||
const blob = new Blob([svgContent], { type: "image/svg+xml" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = defaultPath;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
toast(t("diagram.exportedSvg"));
|
||||
} catch (e: any) {
|
||||
toast(t("diagram.exportSvgFailed", { message: e?.message || String(e) }), 5000);
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
|
||||
import * as api from "@/lib/tauri";
|
||||
import * as api from "@/lib/api";
|
||||
import {
|
||||
diffColumns, diffIndexes, diffTables, generateSyncSql,
|
||||
type TableDiff,
|
||||
|
||||
@@ -28,8 +28,8 @@ import { buildAiContext, runAiStream, type AiAction } from "@/lib/ai";
|
||||
import {
|
||||
listDatabases, redisListDatabases, mongoListDatabases, aiTestConnection, aiCancelStream,
|
||||
saveAiConversation, loadAiConversations, deleteAiConversation, type AiConversation,
|
||||
} from "@/lib/tauri";
|
||||
import type { AiMessage } from "@/lib/tauri";
|
||||
} from "@/lib/api";
|
||||
import type { AiMessage } from "@/lib/api";
|
||||
import type { ConnectionConfig, QueryTab } from "@/types/database";
|
||||
|
||||
const { t } = useI18n();
|
||||
@@ -620,15 +620,15 @@ function formatInlineText(text: string): string {
|
||||
</div>
|
||||
<div class="grid grid-cols-3 items-center gap-3">
|
||||
<Label class="text-right text-xs">API Key</Label>
|
||||
<Input v-model="tempApiKey" type="password" class="col-span-2 h-8 text-xs" />
|
||||
<Input v-model="tempApiKey" type="password" autocomplete="off" class="col-span-2 h-8 text-xs" />
|
||||
</div>
|
||||
<div class="grid grid-cols-3 items-center gap-3">
|
||||
<Label class="text-right text-xs">Endpoint</Label>
|
||||
<Input v-model="tempEndpoint" placeholder="https://api.openai.com/v1" class="col-span-2 h-8 text-xs" />
|
||||
<Input v-model="tempEndpoint" placeholder="https://api.openai.com/v1" autocomplete="off" class="col-span-2 h-8 text-xs" />
|
||||
</div>
|
||||
<div class="grid grid-cols-3 items-center gap-3">
|
||||
<Label class="text-right text-xs">Model</Label>
|
||||
<Input v-model="tempModel" class="col-span-2 h-8 text-xs" />
|
||||
<Input v-model="tempModel" autocomplete="off" class="col-span-2 h-8 text-xs" />
|
||||
</div>
|
||||
<div v-if="tempProvider !== 'claude'" class="grid grid-cols-3 items-center gap-3">
|
||||
<Label class="text-right text-xs">API</Label>
|
||||
@@ -640,7 +640,7 @@ function formatInlineText(text: string): string {
|
||||
</div>
|
||||
<DialogFooter class="flex items-center gap-2">
|
||||
<div class="flex-1 flex items-center gap-2">
|
||||
<Button size="sm" variant="outline" :disabled="testingAi || !tempApiKey.trim() || !tempEndpoint.trim() || !tempModel.trim()" @click="testAiConnection">
|
||||
<Button size="sm" variant="outline" :disabled="testingAi || !tempApiKey?.trim() || !tempEndpoint?.trim() || !tempModel?.trim()" @click="testAiConnection">
|
||||
<Loader2 v-if="testingAi" class="h-3 w-3 animate-spin mr-1" />
|
||||
{{ t('connection.test') }}
|
||||
</Button>
|
||||
|
||||
@@ -22,9 +22,8 @@ import {
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import DangerConfirmDialog from "@/components/editor/DangerConfirmDialog.vue";
|
||||
import type { QueryResult, ColumnInfo, DatabaseType } from "@/types/database";
|
||||
import { save as savePath } from "@tauri-apps/plugin-dialog";
|
||||
import { writeTextFile } from "@tauri-apps/plugin-fs";
|
||||
import * as api from "@/lib/tauri";
|
||||
import { isTauriRuntime } from "@/lib/tauriRuntime";
|
||||
import * as api from "@/lib/api";
|
||||
import {
|
||||
extractSelection,
|
||||
formatSelectionAsCsv,
|
||||
@@ -953,14 +952,30 @@ function copyAll() {
|
||||
copyText(`${header}\n${body}`);
|
||||
}
|
||||
|
||||
async function saveFileContent(content: string, defaultFileName: string, filterName: string, filterExt: string) {
|
||||
if (isTauriRuntime()) {
|
||||
const { save } = await import("@tauri-apps/plugin-dialog");
|
||||
const { writeTextFile } = await import("@tauri-apps/plugin-fs");
|
||||
const path = await save({ defaultPath: defaultFileName, filters: [{ name: filterName, extensions: [filterExt] }] });
|
||||
if (path) await writeTextFile(path, content);
|
||||
} else {
|
||||
const blob = new Blob([content], { type: "text/plain" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = defaultFileName;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
}
|
||||
|
||||
async function exportCsv() {
|
||||
const escape = (v: string) => `"${v.replace(/"/g, '""')}"`;
|
||||
const header = props.result.columns.map(escape).join(",");
|
||||
const body = displayItems.value
|
||||
.map((item) => item.data.map((c) => escape(formatCell(c))).join(","))
|
||||
.join("\n");
|
||||
const path = await savePath({ filters: [{ name: "CSV", extensions: ["csv"] }] });
|
||||
if (path) await writeTextFile(path, `${header}\n${body}`);
|
||||
await saveFileContent(`${header}\n${body}`, "export.csv", "CSV", "csv");
|
||||
}
|
||||
|
||||
async function exportJson() {
|
||||
@@ -969,16 +984,14 @@ async function exportJson() {
|
||||
props.result.columns.forEach((col, i) => { obj[col] = item.data[i]; });
|
||||
return obj;
|
||||
});
|
||||
const path = await savePath({ filters: [{ name: "JSON", extensions: ["json"] }] });
|
||||
if (path) await writeTextFile(path, JSON.stringify(data, null, 2));
|
||||
await saveFileContent(JSON.stringify(data, null, 2), "export.json", "JSON", "json");
|
||||
}
|
||||
|
||||
async function exportMarkdown() {
|
||||
const cols = props.result.columns;
|
||||
const visibleRows = displayItems.value.map((item) => item.data);
|
||||
const md = formatMarkdownTable({ columns: cols, rows: visibleRows });
|
||||
const path = await savePath({ filters: [{ name: "Markdown", extensions: ["md"] }] });
|
||||
if (path) await writeTextFile(path, md);
|
||||
await saveFileContent(md, "export.md", "Markdown", "md");
|
||||
}
|
||||
|
||||
const sqlOneLiner = computed(() => props.sql?.replace(/\s+/g, " ").trim() || "");
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { open as openFileDialog } from "@tauri-apps/plugin-dialog";
|
||||
import { isTauriRuntime } from "@/lib/tauriRuntime";
|
||||
import {
|
||||
Dialog, DialogHeader, DialogTitle, DialogFooter, DialogScrollContent,
|
||||
} from "@/components/ui/dialog";
|
||||
@@ -18,7 +18,7 @@ import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import { autoMapImportColumns } from "@/lib/tableImport";
|
||||
import type { ColumnInfo } from "@/types/database";
|
||||
import * as api from "@/lib/tauri";
|
||||
import * as api from "@/lib/api";
|
||||
|
||||
const { t } = useI18n();
|
||||
const store = useConnectionStore();
|
||||
@@ -119,7 +119,9 @@ async function loadTargetColumns() {
|
||||
}
|
||||
|
||||
async function selectFile() {
|
||||
const selected = await openFileDialog({
|
||||
if (!isTauriRuntime()) return;
|
||||
const { open } = await import("@tauri-apps/plugin-dialog");
|
||||
const selected = await open({
|
||||
multiple: false,
|
||||
filters: [
|
||||
{ name: "Data files", extensions: ["csv", "tsv", "json", "xlsx", "xlsm", "xls"] },
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Dialog, DialogFooter, DialogHeader, DialogScrollContent, DialogTitle }
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import * as api from "@/lib/tauri";
|
||||
import * as api from "@/lib/api";
|
||||
import {
|
||||
analyzeFieldLineage,
|
||||
summarizeLineageCounts,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { RefreshCw, Trash2, Plus, Save, ChevronLeft, ChevronRight } from "lucide
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import DangerConfirmDialog from "@/components/editor/DangerConfirmDialog.vue";
|
||||
import * as api from "@/lib/tauri";
|
||||
import * as api from "@/lib/api";
|
||||
import JsonEditNode from "./JsonEditNode.vue";
|
||||
import type { EditNode } from "@/types/editor";
|
||||
import { Splitpanes, Pane } from "splitpanes";
|
||||
|
||||
@@ -8,8 +8,8 @@ import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import RedisValueViewer from "./RedisValueViewer.vue";
|
||||
import * as api from "@/lib/tauri";
|
||||
import type { RedisKeyInfo } from "@/lib/tauri";
|
||||
import * as api from "@/lib/api";
|
||||
import type { RedisKeyInfo } from "@/lib/api";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import DangerConfirmDialog from "@/components/editor/DangerConfirmDialog.vue";
|
||||
import * as api from "@/lib/tauri";
|
||||
import type { RedisValue } from "@/lib/tauri";
|
||||
import * as api from "@/lib/api";
|
||||
import type { RedisValue } from "@/lib/api";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Dialog, DialogFooter, DialogHeader, DialogScrollContent, DialogTitle }
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import * as api from "@/lib/tauri";
|
||||
import * as api from "@/lib/api";
|
||||
import {
|
||||
buildDatabaseSearchSql,
|
||||
buildSearchResultWhere,
|
||||
|
||||
@@ -17,7 +17,7 @@ import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { useQueryStore } from "@/stores/queryStore";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import type { DatabaseType, QueryResult, TreeNode, TreeNodeType } from "@/types/database";
|
||||
import * as api from "@/lib/tauri";
|
||||
import * as api from "@/lib/api";
|
||||
import {
|
||||
DATABASE_EXPORT_PAGE_SIZE,
|
||||
DATABASE_EXPORT_ROW_LIMIT,
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
} from "@/lib/databaseExport";
|
||||
import { qualifiedTableName as buildQualifiedTableName, quoteTableIdentifier } from "@/lib/tableSelectSql";
|
||||
import { treeNodeRowAction } from "@/lib/treeNodeClick";
|
||||
import { isTauriRuntime } from "@/lib/tauriRuntime";
|
||||
import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
|
||||
import {
|
||||
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter,
|
||||
@@ -357,6 +358,23 @@ async function fetchExportTableRows(
|
||||
};
|
||||
}
|
||||
|
||||
async function saveFileContent(content: string, defaultFileName: string, filterName: string, filterExt: string) {
|
||||
if (isTauriRuntime()) {
|
||||
const { save } = await import("@tauri-apps/plugin-dialog");
|
||||
const { writeTextFile } = await import("@tauri-apps/plugin-fs");
|
||||
const path = await save({ defaultPath: defaultFileName, filters: [{ name: filterName, extensions: [filterExt] }] });
|
||||
if (path) await writeTextFile(path, content);
|
||||
} else {
|
||||
const blob = new Blob([content], { type: "text/plain" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = defaultFileName;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
}
|
||||
|
||||
async function exportDatabase() {
|
||||
const node = props.node;
|
||||
if (!(node.type === "database" || node.type === "schema") || !node.connectionId || !node.database) return;
|
||||
@@ -393,15 +411,7 @@ async function exportDatabase() {
|
||||
rowLimitPerTable: DATABASE_EXPORT_ROW_LIMIT,
|
||||
});
|
||||
|
||||
const { save } = await import("@tauri-apps/plugin-dialog");
|
||||
const { writeTextFile } = await import("@tauri-apps/plugin-fs");
|
||||
const path = await save({
|
||||
defaultPath: `${safeFileName(scopeName)}.sql`,
|
||||
filters: [{ name: "SQL", extensions: ["sql"] }],
|
||||
});
|
||||
if (!path) return;
|
||||
|
||||
await writeTextFile(path, content);
|
||||
await saveFileContent(content, `${safeFileName(scopeName)}.sql`, "SQL", "sql");
|
||||
toast(t("contextMenu.exportDatabaseSuccess", { count: exportedTables.length, limit: DATABASE_EXPORT_ROW_LIMIT }), 3000);
|
||||
} catch (e: any) {
|
||||
console.error("Export database failed:", e);
|
||||
@@ -417,10 +427,7 @@ async function exportStructure() {
|
||||
try {
|
||||
await connectionStore.ensureConnected(node.connectionId);
|
||||
const ddl = await api.getTableDdl(node.connectionId, node.database, node.schema || node.database, node.label);
|
||||
const { save } = await import("@tauri-apps/plugin-dialog");
|
||||
const { writeTextFile } = await import("@tauri-apps/plugin-fs");
|
||||
const path = await save({ defaultPath: `${node.label}.sql`, filters: [{ name: "SQL", extensions: ["sql"] }] });
|
||||
if (path) await writeTextFile(path, ddl + "\n");
|
||||
await saveFileContent(ddl + "\n", `${node.label}.sql`, "SQL", "sql");
|
||||
} catch (e: any) {
|
||||
console.error("Export structure failed:", e);
|
||||
}
|
||||
@@ -439,9 +446,6 @@ async function exportData(format: "csv" | "json" | "sql") {
|
||||
: quoteIdent(node.label);
|
||||
const result = await api.executeQuery(node.connectionId, node.database, `SELECT * FROM ${qualifiedName}`);
|
||||
|
||||
const { save } = await import("@tauri-apps/plugin-dialog");
|
||||
const { writeTextFile } = await import("@tauri-apps/plugin-fs");
|
||||
|
||||
let content: string;
|
||||
let ext: string;
|
||||
|
||||
@@ -473,8 +477,7 @@ async function exportData(format: "csv" | "json" | "sql") {
|
||||
content = lines.join("\n");
|
||||
}
|
||||
|
||||
const path = await save({ defaultPath: `${node.label}.${ext}`, filters: [{ name: ext.toUpperCase(), extensions: [ext] }] });
|
||||
if (path) await writeTextFile(path, content);
|
||||
await saveFileContent(content, `${node.label}.${ext}`, ext.toUpperCase(), ext);
|
||||
} catch (e: any) {
|
||||
console.error("Export data failed:", e);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { open as openDialog } from "@tauri-apps/plugin-dialog";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { isTauriRuntime } from "@/lib/tauriRuntime";
|
||||
import {
|
||||
Dialog, DialogFooter, DialogHeader, DialogScrollContent, DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
type SqlFilePreview,
|
||||
type SqlFileProgress,
|
||||
type SqlFileStatus,
|
||||
} from "@/lib/tauri";
|
||||
} from "@/lib/api";
|
||||
import { Check, CheckSquare, FileCode, FolderOpen, Loader2, Play, Square, X } from "lucide-vue-next";
|
||||
|
||||
const { t } = useI18n();
|
||||
@@ -214,9 +214,11 @@ async function loadPreview(path: string) {
|
||||
|
||||
async function selectFile() {
|
||||
if (running.value) return;
|
||||
if (!isTauriRuntime()) return;
|
||||
selectingFile.value = true;
|
||||
try {
|
||||
const selected = await openDialog({
|
||||
const { open } = await import("@tauri-apps/plugin-dialog");
|
||||
const selected = await open({
|
||||
multiple: false,
|
||||
filters: [{ name: "SQL", extensions: ["sql"] }],
|
||||
});
|
||||
|
||||
@@ -24,7 +24,7 @@ import { useToast } from "@/composables/useToast";
|
||||
import { buildTableStructureChangeSql, type EditableStructureColumn, type EditableStructureIndex } from "@/lib/tableStructureEditorSql";
|
||||
import { createColumnDrafts, createIndexDrafts, toColumnNames } from "@/lib/tableStructureEditorState";
|
||||
import type { ForeignKeyInfo, TriggerInfo } from "@/types/database";
|
||||
import * as api from "@/lib/tauri";
|
||||
import * as api from "@/lib/api";
|
||||
|
||||
const { t } = useI18n();
|
||||
const store = useConnectionStore();
|
||||
|
||||
@@ -13,8 +13,8 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
|
||||
import * as api from "@/lib/tauri";
|
||||
import type { TransferProgress } from "@/lib/tauri";
|
||||
import * as api from "@/lib/api";
|
||||
import type { TransferProgress } from "@/lib/api";
|
||||
import type { DatabaseType } from "@/types/database";
|
||||
import { nextTransferTerminalState } from "@/lib/transferProgressState";
|
||||
import {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import type { AiConfig } from "@/stores/settingsStore";
|
||||
import type { ColumnInfo, ConnectionConfig, DatabaseType, ForeignKeyInfo, IndexInfo, QueryResult, QueryTab } from "@/types/database";
|
||||
import * as api from "@/lib/tauri";
|
||||
import * as api from "@/lib/api";
|
||||
import { currentLocale } from "@/i18n";
|
||||
|
||||
export type AiAction = "generate" | "explain" | "optimize" | "fix" | "convert" | "sampleData";
|
||||
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
import { isTauriRuntime } from "./tauriRuntime";
|
||||
import type * as TauriModule from "./tauri";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lazy backend resolution (avoids top-level await)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type Backend = typeof TauriModule;
|
||||
|
||||
let _backend: Backend | null = null;
|
||||
|
||||
async function getBackend(): Promise<Backend> {
|
||||
if (_backend) return _backend;
|
||||
_backend = isTauriRuntime(globalThis)
|
||||
? await import("./tauri")
|
||||
: await import("./http");
|
||||
return _backend;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: create a forwarding function that lazily resolves the backend
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function forward<K extends keyof Backend>(name: K): Backend[K] {
|
||||
return (async (...args: unknown[]) => {
|
||||
const b = await getBackend();
|
||||
return (b[name] as (...a: unknown[]) => unknown)(...args);
|
||||
}) as unknown as Backend[K];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Re-export all functions via lazy forwarding
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Connection
|
||||
export const testConnection = forward("testConnection");
|
||||
export const connectDb = forward("connectDb");
|
||||
export const disconnectDb = forward("disconnectDb");
|
||||
export const saveConnections = forward("saveConnections");
|
||||
export const loadConnections = forward("loadConnections");
|
||||
|
||||
// Schema
|
||||
export const listDatabases = forward("listDatabases");
|
||||
export const listSchemas = forward("listSchemas");
|
||||
export const listTables = forward("listTables");
|
||||
export const getColumns = forward("getColumns");
|
||||
export const listIndexes = forward("listIndexes");
|
||||
export const listForeignKeys = forward("listForeignKeys");
|
||||
export const listTriggers = forward("listTriggers");
|
||||
export const getTableDdl = forward("getTableDdl");
|
||||
|
||||
// Query
|
||||
export const executeQuery = forward("executeQuery");
|
||||
export const executeMulti = forward("executeMulti");
|
||||
export const executeBatch = forward("executeBatch");
|
||||
export const executeScript = forward("executeScript");
|
||||
export const cancelQuery = forward("cancelQuery");
|
||||
|
||||
// AI
|
||||
export const aiComplete = forward("aiComplete");
|
||||
export const aiStream = forward("aiStream");
|
||||
export const aiCancelStream = forward("aiCancelStream");
|
||||
export const aiTestConnection = forward("aiTestConnection");
|
||||
export const saveAiConfig = forward("saveAiConfig");
|
||||
export const loadAiConfig = forward("loadAiConfig");
|
||||
export const saveAiConversation = forward("saveAiConversation");
|
||||
export const loadAiConversations = forward("loadAiConversations");
|
||||
export const deleteAiConversation = forward("deleteAiConversation");
|
||||
|
||||
// SQL File Execution
|
||||
export const previewSqlFile = forward("previewSqlFile");
|
||||
export const executeSqlFile = forward("executeSqlFile");
|
||||
export const cancelSqlFileExecution = forward("cancelSqlFileExecution");
|
||||
export const listenSqlFileProgress = forward("listenSqlFileProgress");
|
||||
|
||||
// Data Transfer
|
||||
export const startTransfer = forward("startTransfer");
|
||||
export const cancelTransfer = forward("cancelTransfer");
|
||||
|
||||
// Table File Import
|
||||
export const previewTableImportFile = forward("previewTableImportFile");
|
||||
export const importTableFile = forward("importTableFile");
|
||||
export const cancelTableImport = forward("cancelTableImport");
|
||||
|
||||
// Redis
|
||||
export const redisListDatabases = forward("redisListDatabases");
|
||||
export const redisScanKeys = forward("redisScanKeys");
|
||||
export const redisGetValue = forward("redisGetValue");
|
||||
export const redisSetString = forward("redisSetString");
|
||||
export const redisDeleteKey = forward("redisDeleteKey");
|
||||
export const redisHashSet = forward("redisHashSet");
|
||||
export const redisHashDel = forward("redisHashDel");
|
||||
export const redisListPush = forward("redisListPush");
|
||||
export const redisListRemove = forward("redisListRemove");
|
||||
export const redisSetAdd = forward("redisSetAdd");
|
||||
export const redisSetRemove = forward("redisSetRemove");
|
||||
|
||||
// MongoDB
|
||||
export const mongoListDatabases = forward("mongoListDatabases");
|
||||
export const mongoListCollections = forward("mongoListCollections");
|
||||
export const mongoFindDocuments = forward("mongoFindDocuments");
|
||||
export const mongoInsertDocument = forward("mongoInsertDocument");
|
||||
export const mongoUpdateDocument = forward("mongoUpdateDocument");
|
||||
export const mongoDeleteDocument = forward("mongoDeleteDocument");
|
||||
|
||||
// History
|
||||
export const saveHistory = forward("saveHistory");
|
||||
export const loadHistory = forward("loadHistory");
|
||||
export const clearHistory = forward("clearHistory");
|
||||
export const deleteHistoryEntry = forward("deleteHistoryEntry");
|
||||
|
||||
// Updates
|
||||
export const checkForUpdates = forward("checkForUpdates");
|
||||
|
||||
// Layout
|
||||
export const saveSidebarLayout = forward("saveSidebarLayout");
|
||||
export const loadSidebarLayout = forward("loadSidebarLayout");
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Re-export all types from tauri.ts (shared between both backends)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export type {
|
||||
AiMessage,
|
||||
AiCompletionRequest,
|
||||
AiStreamChunk,
|
||||
AiChatMessage,
|
||||
AiConversation,
|
||||
UpdateInfo,
|
||||
RedisKeyInfo,
|
||||
RedisValue,
|
||||
RedisScanResult,
|
||||
MongoDocumentResult,
|
||||
HistoryEntry,
|
||||
SqlFileStatus,
|
||||
SqlFileRequest,
|
||||
SqlFilePreview,
|
||||
SqlFileProgress,
|
||||
TransferRequest,
|
||||
TransferProgress,
|
||||
TableImportMode,
|
||||
TableImportStatus,
|
||||
TableImportColumnMapping,
|
||||
TableImportPreview,
|
||||
TableImportRequest,
|
||||
TableImportSummary,
|
||||
TableImportProgress,
|
||||
} from "./tauri";
|
||||
+566
@@ -0,0 +1,566 @@
|
||||
import type {
|
||||
ConnectionConfig,
|
||||
DatabaseInfo,
|
||||
TableInfo,
|
||||
ColumnInfo,
|
||||
IndexInfo,
|
||||
ForeignKeyInfo,
|
||||
TriggerInfo,
|
||||
QueryResult,
|
||||
SidebarLayout,
|
||||
} from "@/types/database";
|
||||
import type { AiConfig } from "@/stores/settingsStore";
|
||||
import type {
|
||||
AiCompletionRequest,
|
||||
AiStreamChunk,
|
||||
AiConversation,
|
||||
UpdateInfo,
|
||||
RedisValue,
|
||||
RedisScanResult,
|
||||
MongoDocumentResult,
|
||||
HistoryEntry,
|
||||
SqlFileRequest,
|
||||
SqlFilePreview,
|
||||
SqlFileProgress,
|
||||
TransferRequest,
|
||||
TransferProgress,
|
||||
TableImportPreview,
|
||||
TableImportRequest,
|
||||
TableImportSummary,
|
||||
TableImportProgress,
|
||||
} from "./tauri";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function post<T>(url: string, body: unknown): Promise<T> {
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
return res.json();
|
||||
}
|
||||
|
||||
async function get<T>(url: string): Promise<T> {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
return res.json();
|
||||
}
|
||||
|
||||
async function del<T>(url: string): Promise<T> {
|
||||
const res = await fetch(url, { method: "DELETE" });
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
return res.json();
|
||||
}
|
||||
|
||||
function qs(params: Record<string, string | number | undefined>): string {
|
||||
const sp = new URLSearchParams();
|
||||
for (const [k, v] of Object.entries(params)) {
|
||||
if (v !== undefined && v !== null) sp.set(k, String(v));
|
||||
}
|
||||
return sp.toString();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Connection
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function testConnection(config: ConnectionConfig): Promise<string> {
|
||||
return post("/api/connection/test", { config });
|
||||
}
|
||||
|
||||
export async function connectDb(config: ConnectionConfig): Promise<string> {
|
||||
return post("/api/connection/connect", { config });
|
||||
}
|
||||
|
||||
export async function disconnectDb(connectionId: string): Promise<void> {
|
||||
return post("/api/connection/disconnect", { connectionId });
|
||||
}
|
||||
|
||||
export async function saveConnections(configs: ConnectionConfig[]): Promise<void> {
|
||||
return post("/api/connection/save", { configs });
|
||||
}
|
||||
|
||||
export async function loadConnections(): Promise<ConnectionConfig[]> {
|
||||
return get("/api/connection/list");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Schema
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function listDatabases(connectionId: string): Promise<DatabaseInfo[]> {
|
||||
return get(`/api/schema/databases?${qs({ connection_id: connectionId })}`);
|
||||
}
|
||||
|
||||
export async function listSchemas(connectionId: string, database: string): Promise<string[]> {
|
||||
return get(`/api/schema/schemas?${qs({ connection_id: connectionId, database })}`);
|
||||
}
|
||||
|
||||
export async function listTables(connectionId: string, database: string, schema: string): Promise<TableInfo[]> {
|
||||
return get(`/api/schema/tables?${qs({ connection_id: connectionId, database, schema })}`);
|
||||
}
|
||||
|
||||
export async function getColumns(
|
||||
connectionId: string,
|
||||
database: string,
|
||||
schema: string,
|
||||
table: string,
|
||||
): Promise<ColumnInfo[]> {
|
||||
return get(`/api/schema/columns?${qs({ connection_id: connectionId, database, schema, table })}`);
|
||||
}
|
||||
|
||||
export async function listIndexes(
|
||||
connectionId: string,
|
||||
database: string,
|
||||
schema: string,
|
||||
table: string,
|
||||
): Promise<IndexInfo[]> {
|
||||
return get(`/api/schema/indexes?${qs({ connection_id: connectionId, database, schema, table })}`);
|
||||
}
|
||||
|
||||
export async function listForeignKeys(
|
||||
connectionId: string,
|
||||
database: string,
|
||||
schema: string,
|
||||
table: string,
|
||||
): Promise<ForeignKeyInfo[]> {
|
||||
return get(`/api/schema/foreign-keys?${qs({ connection_id: connectionId, database, schema, table })}`);
|
||||
}
|
||||
|
||||
export async function listTriggers(
|
||||
connectionId: string,
|
||||
database: string,
|
||||
schema: string,
|
||||
table: string,
|
||||
): Promise<TriggerInfo[]> {
|
||||
return get(`/api/schema/triggers?${qs({ connection_id: connectionId, database, schema, table })}`);
|
||||
}
|
||||
|
||||
export async function getTableDdl(
|
||||
connectionId: string,
|
||||
database: string,
|
||||
schema: string,
|
||||
table: string,
|
||||
): Promise<string> {
|
||||
return get(`/api/schema/ddl?${qs({ connection_id: connectionId, database, schema, table })}`);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Query
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function executeQuery(
|
||||
connectionId: string,
|
||||
database: string,
|
||||
sql: string,
|
||||
executionId?: string,
|
||||
): Promise<QueryResult> {
|
||||
return post("/api/query/execute", { connectionId, database, sql, executionId });
|
||||
}
|
||||
|
||||
export async function executeMulti(
|
||||
connectionId: string,
|
||||
database: string,
|
||||
sql: string,
|
||||
executionId?: string,
|
||||
): Promise<QueryResult[]> {
|
||||
return post("/api/query/execute-multi", { connectionId, database, sql, executionId });
|
||||
}
|
||||
|
||||
export async function executeBatch(
|
||||
connectionId: string,
|
||||
database: string,
|
||||
statements: string[],
|
||||
): Promise<QueryResult> {
|
||||
return post("/api/query/execute-batch", { connectionId, database, statements });
|
||||
}
|
||||
|
||||
export async function executeScript(
|
||||
connectionId: string,
|
||||
database: string,
|
||||
sql: string,
|
||||
): Promise<QueryResult> {
|
||||
return post("/api/query/execute-script", { connectionId, database, sql });
|
||||
}
|
||||
|
||||
export async function cancelQuery(executionId: string): Promise<boolean> {
|
||||
return post("/api/query/cancel", { executionId });
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AI
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function aiComplete(request: AiCompletionRequest): Promise<string> {
|
||||
return post("/api/ai/complete", { request });
|
||||
}
|
||||
|
||||
export async function aiStream(
|
||||
sessionId: string,
|
||||
request: AiCompletionRequest,
|
||||
onChunk: (chunk: AiStreamChunk) => void,
|
||||
): Promise<void> {
|
||||
const res = await fetch("/api/ai/stream", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ session_id: sessionId, request }),
|
||||
});
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
|
||||
const reader = res.body!.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
|
||||
const lines = buffer.split("\n");
|
||||
buffer = lines.pop() || "";
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith("data:")) {
|
||||
const data = line.slice(5).trim();
|
||||
if (data && data !== "[DONE]") {
|
||||
try {
|
||||
const chunk: AiStreamChunk = JSON.parse(data);
|
||||
onChunk(chunk);
|
||||
if (chunk.done) return;
|
||||
} catch {
|
||||
// skip malformed JSON
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function aiCancelStream(sessionId: string): Promise<boolean> {
|
||||
return post("/api/ai/cancel-stream", { sessionId });
|
||||
}
|
||||
|
||||
export async function aiTestConnection(config: AiConfig): Promise<string> {
|
||||
return post("/api/ai/test-connection", { config });
|
||||
}
|
||||
|
||||
export async function saveAiConfig(config: AiConfig): Promise<void> {
|
||||
return post("/api/ai/config", { config });
|
||||
}
|
||||
|
||||
export async function loadAiConfig(): Promise<AiConfig | null> {
|
||||
return get("/api/ai/config");
|
||||
}
|
||||
|
||||
// --- AI Conversations ---
|
||||
|
||||
export async function saveAiConversation(conversation: AiConversation): Promise<void> {
|
||||
return post("/api/ai/conversation", { conversation });
|
||||
}
|
||||
|
||||
export async function loadAiConversations(): Promise<AiConversation[]> {
|
||||
return get("/api/ai/conversations");
|
||||
}
|
||||
|
||||
export async function deleteAiConversation(id: string): Promise<void> {
|
||||
return del(`/api/ai/conversation/${id}`);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SQL File Execution
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function previewSqlFile(fileOrPath: string | File): Promise<SqlFilePreview> {
|
||||
if (typeof fileOrPath === "string") {
|
||||
// In web mode a raw path is not useful; throw a clear error
|
||||
throw new Error("previewSqlFile in web mode requires a File object, not a file path");
|
||||
}
|
||||
const formData = new FormData();
|
||||
formData.append("file", fileOrPath);
|
||||
const res = await fetch("/api/sql-file/preview", { method: "POST", body: formData });
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function executeSqlFile(request: SqlFileRequest): Promise<void> {
|
||||
return post("/api/sql-file/execute", { request });
|
||||
}
|
||||
|
||||
export async function cancelSqlFileExecution(executionId: string): Promise<boolean> {
|
||||
return post("/api/sql-file/cancel", { executionId });
|
||||
}
|
||||
|
||||
export async function listenSqlFileProgress(
|
||||
_handler: (progress: SqlFileProgress) => void,
|
||||
): Promise<() => void> {
|
||||
// For HTTP mode we need an executionId, but the tauri API does not take one.
|
||||
// The SSE endpoint requires a specific executionId. As a workaround we return
|
||||
// a no-op unlisten; callers that need progress in web mode should use
|
||||
// listenSqlFileProgressById instead.
|
||||
return () => {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Web-specific: listen to SQL file execution progress for a given executionId via SSE.
|
||||
*/
|
||||
export function listenSqlFileProgressById(
|
||||
executionId: string,
|
||||
handler: (progress: SqlFileProgress) => void,
|
||||
): () => void {
|
||||
const es = new EventSource(`/api/sql-file/progress/${executionId}`);
|
||||
es.onmessage = (e) => {
|
||||
const progress: SqlFileProgress = JSON.parse(e.data);
|
||||
handler(progress);
|
||||
if (progress.status === "done" || progress.status === "error" || progress.status === "cancelled") {
|
||||
es.close();
|
||||
}
|
||||
};
|
||||
es.onerror = () => {
|
||||
es.close();
|
||||
};
|
||||
return () => es.close();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Data Transfer
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function startTransfer(
|
||||
request: TransferRequest,
|
||||
onProgress: (progress: TransferProgress) => void,
|
||||
): Promise<void> {
|
||||
// 1. POST to start the transfer
|
||||
const res = await fetch("/api/transfer/start", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ request }),
|
||||
});
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
|
||||
// 2. SSE to listen for progress
|
||||
return new Promise((resolve, reject) => {
|
||||
const es = new EventSource(`/api/transfer/progress/${request.transferId}`);
|
||||
es.onmessage = (e) => {
|
||||
const progress: TransferProgress = JSON.parse(e.data);
|
||||
onProgress(progress);
|
||||
if (progress.status === "done" || progress.status === "error" || progress.status === "cancelled") {
|
||||
es.close();
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
es.onerror = () => {
|
||||
es.close();
|
||||
reject(new Error("Transfer SSE connection failed"));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export async function cancelTransfer(transferId: string): Promise<void> {
|
||||
return post("/api/transfer/cancel", { transferId });
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Table File Import
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function previewTableImportFile(fileOrPath: string | File): Promise<TableImportPreview> {
|
||||
if (typeof fileOrPath === "string") {
|
||||
throw new Error("previewTableImportFile in web mode requires a File object, not a file path");
|
||||
}
|
||||
const formData = new FormData();
|
||||
formData.append("file", fileOrPath);
|
||||
const res = await fetch("/api/import/preview", { method: "POST", body: formData });
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function importTableFile(
|
||||
request: TableImportRequest,
|
||||
onProgress: (progress: TableImportProgress) => void,
|
||||
): Promise<TableImportSummary> {
|
||||
// 1. POST to start the import
|
||||
const res = await fetch("/api/import/execute", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ request }),
|
||||
});
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
|
||||
// 2. SSE to listen for progress
|
||||
return new Promise((resolve, reject) => {
|
||||
const es = new EventSource(`/api/import/progress/${request.importId}`);
|
||||
let summary: TableImportSummary | null = null;
|
||||
es.onmessage = (e) => {
|
||||
const progress: TableImportProgress = JSON.parse(e.data);
|
||||
onProgress(progress);
|
||||
if (progress.status === "done") {
|
||||
summary = {
|
||||
importId: progress.importId,
|
||||
rowsImported: progress.rowsImported,
|
||||
totalRows: progress.totalRows,
|
||||
};
|
||||
es.close();
|
||||
resolve(summary);
|
||||
} else if (progress.status === "error" || progress.status === "cancelled") {
|
||||
es.close();
|
||||
reject(new Error(progress.error || "Import failed"));
|
||||
}
|
||||
};
|
||||
es.onerror = () => {
|
||||
es.close();
|
||||
reject(new Error("Import SSE connection failed"));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export async function cancelTableImport(importId: string): Promise<boolean> {
|
||||
return post("/api/import/cancel", { importId });
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Redis
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function redisListDatabases(connectionId: string): Promise<number[]> {
|
||||
return post("/api/redis/list-databases", { connectionId });
|
||||
}
|
||||
|
||||
export async function redisScanKeys(
|
||||
connectionId: string,
|
||||
db: number,
|
||||
cursor: number,
|
||||
pattern: string,
|
||||
count: number,
|
||||
): Promise<RedisScanResult> {
|
||||
return post("/api/redis/scan-keys", { connectionId, db, cursor, pattern, count });
|
||||
}
|
||||
|
||||
export async function redisGetValue(connectionId: string, key: string): Promise<RedisValue> {
|
||||
return post("/api/redis/get-value", { connectionId, key });
|
||||
}
|
||||
|
||||
export async function redisSetString(connectionId: string, key: string, value: string, ttl?: number): Promise<void> {
|
||||
return post("/api/redis/set-string", { connectionId, key, value, ttl });
|
||||
}
|
||||
|
||||
export async function redisDeleteKey(connectionId: string, key: string): Promise<void> {
|
||||
return post("/api/redis/delete-key", { connectionId, key });
|
||||
}
|
||||
|
||||
export async function redisHashSet(connectionId: string, key: string, field: string, value: string): Promise<void> {
|
||||
return post("/api/redis/hash-set", { connectionId, key, field, value });
|
||||
}
|
||||
|
||||
export async function redisHashDel(connectionId: string, key: string, field: string): Promise<void> {
|
||||
return post("/api/redis/hash-del", { connectionId, key, field });
|
||||
}
|
||||
|
||||
export async function redisListPush(connectionId: string, key: string, value: string): Promise<void> {
|
||||
return post("/api/redis/list-push", { connectionId, key, value });
|
||||
}
|
||||
|
||||
export async function redisListRemove(connectionId: string, key: string, index: number): Promise<void> {
|
||||
return post("/api/redis/list-remove", { connectionId, key, index });
|
||||
}
|
||||
|
||||
export async function redisSetAdd(connectionId: string, key: string, member: string): Promise<void> {
|
||||
return post("/api/redis/set-add", { connectionId, key, member });
|
||||
}
|
||||
|
||||
export async function redisSetRemove(connectionId: string, key: string, member: string): Promise<void> {
|
||||
return post("/api/redis/set-remove", { connectionId, key, member });
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// MongoDB
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function mongoListDatabases(connectionId: string): Promise<string[]> {
|
||||
return post("/api/mongo/list-databases", { connectionId });
|
||||
}
|
||||
|
||||
export async function mongoListCollections(connectionId: string, database: string): Promise<string[]> {
|
||||
return post("/api/mongo/list-collections", { connectionId, database });
|
||||
}
|
||||
|
||||
export async function mongoFindDocuments(
|
||||
connectionId: string,
|
||||
database: string,
|
||||
collection: string,
|
||||
skip: number,
|
||||
limit: number,
|
||||
): Promise<MongoDocumentResult> {
|
||||
return post("/api/mongo/find-documents", { connectionId, database, collection, skip, limit });
|
||||
}
|
||||
|
||||
export async function mongoInsertDocument(
|
||||
connectionId: string,
|
||||
database: string,
|
||||
collection: string,
|
||||
docJson: string,
|
||||
): Promise<string> {
|
||||
return post("/api/mongo/insert-document", { connectionId, database, collection, docJson });
|
||||
}
|
||||
|
||||
export async function mongoUpdateDocument(
|
||||
connectionId: string,
|
||||
database: string,
|
||||
collection: string,
|
||||
id: string,
|
||||
docJson: string,
|
||||
): Promise<number> {
|
||||
return post("/api/mongo/update-document", { connectionId, database, collection, id, docJson });
|
||||
}
|
||||
|
||||
export async function mongoDeleteDocument(
|
||||
connectionId: string,
|
||||
database: string,
|
||||
collection: string,
|
||||
id: string,
|
||||
): Promise<number> {
|
||||
return post("/api/mongo/delete-document", { connectionId, database, collection, id });
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// History
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function saveHistory(entry: HistoryEntry): Promise<void> {
|
||||
return post("/api/history/save", { entry });
|
||||
}
|
||||
|
||||
export async function loadHistory(limit: number, offset: number): Promise<HistoryEntry[]> {
|
||||
return get(`/api/history?${qs({ limit, offset })}`);
|
||||
}
|
||||
|
||||
export async function clearHistory(): Promise<void> {
|
||||
return del("/api/history");
|
||||
}
|
||||
|
||||
export async function deleteHistoryEntry(id: string): Promise<void> {
|
||||
return del(`/api/history/${id}`);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Updates
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function checkForUpdates(): Promise<UpdateInfo> {
|
||||
return get("/api/update/check");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Layout
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function saveSidebarLayout(layout: SidebarLayout): Promise<void> {
|
||||
return post("/api/layout/sidebar", { layout });
|
||||
}
|
||||
|
||||
export async function loadSidebarLayout(): Promise<SidebarLayout | null> {
|
||||
return get("/api/layout/sidebar");
|
||||
}
|
||||
@@ -17,7 +17,8 @@ import {
|
||||
type DropPosition,
|
||||
} from "@/lib/sidebarLayout";
|
||||
import type { SqlCompletionColumn, SqlCompletionTable } from "@/lib/sqlCompletion";
|
||||
import * as api from "@/lib/tauri";
|
||||
import * as api from "@/lib/api";
|
||||
import { isTauriRuntime } from "@/lib/tauriRuntime";
|
||||
|
||||
const PINNED_TREE_NODES_STORAGE_KEY = "dbx-pinned-tree-nodes";
|
||||
|
||||
@@ -620,23 +621,55 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
}
|
||||
|
||||
async function exportConnectionsToFile(passphrase: string) {
|
||||
const { save } = await import("@tauri-apps/plugin-dialog");
|
||||
const { writeTextFile } = await import("@tauri-apps/plugin-fs");
|
||||
const { encryptConfig } = await import("@/lib/configCrypto");
|
||||
const path = await save({ filters: [{ name: "JSON", extensions: ["json"] }], defaultPath: "dbx-connections.json" });
|
||||
if (!path) return;
|
||||
const exportData = { connections: connections.value, layout: sidebarLayout.value };
|
||||
const json = JSON.stringify(exportData);
|
||||
const payload = await encryptConfig(json, passphrase);
|
||||
await writeTextFile(path, JSON.stringify(payload, null, 2));
|
||||
const content = JSON.stringify(payload, null, 2);
|
||||
|
||||
if (isTauriRuntime()) {
|
||||
const { save } = await import("@tauri-apps/plugin-dialog");
|
||||
const { writeTextFile } = await import("@tauri-apps/plugin-fs");
|
||||
const path = await save({ filters: [{ name: "JSON", extensions: ["json"] }], defaultPath: "dbx-connections.json" });
|
||||
if (!path) return;
|
||||
await writeTextFile(path, content);
|
||||
} else {
|
||||
const blob = new Blob([content], { type: "application/json" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = "dbx-connections.json";
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
}
|
||||
|
||||
async function readImportFile(): Promise<{ content: string; encrypted: boolean } | null> {
|
||||
const { open } = await import("@tauri-apps/plugin-dialog");
|
||||
const { readTextFile } = await import("@tauri-apps/plugin-fs");
|
||||
const path = await open({ filters: [{ name: "JSON", extensions: ["json"] }], multiple: false });
|
||||
if (!path) return null;
|
||||
const content = await readTextFile(path as string);
|
||||
let content: string;
|
||||
|
||||
if (isTauriRuntime()) {
|
||||
const { open } = await import("@tauri-apps/plugin-dialog");
|
||||
const { readTextFile } = await import("@tauri-apps/plugin-fs");
|
||||
const path = await open({ filters: [{ name: "JSON", extensions: ["json"] }], multiple: false });
|
||||
if (!path) return null;
|
||||
content = await readTextFile(path as string);
|
||||
} else {
|
||||
content = await new Promise<string>((resolve, reject) => {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.accept = ".json";
|
||||
input.onchange = () => {
|
||||
const file = input.files?.[0];
|
||||
if (!file) { reject(new Error("No file selected")); return; }
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve(reader.result as string);
|
||||
reader.onerror = () => reject(reader.error);
|
||||
reader.readAsText(file);
|
||||
};
|
||||
input.click();
|
||||
});
|
||||
}
|
||||
|
||||
const { isEncryptedConfig } = await import("@/lib/configCrypto");
|
||||
const parsed = JSON.parse(content);
|
||||
return { content, encrypted: isEncryptedConfig(parsed) };
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
import * as api from "@/lib/tauri";
|
||||
import type { HistoryEntry } from "@/lib/tauri";
|
||||
import * as api from "@/lib/api";
|
||||
import type { HistoryEntry } from "@/lib/api";
|
||||
|
||||
export const useHistoryStore = defineStore("history", () => {
|
||||
const entries = ref<HistoryEntry[]>([]);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { orderPinnedFirst } from "@/lib/pinnedItems";
|
||||
import { canCancelQueryExecution } from "@/lib/queryExecutionState";
|
||||
import { closeAllTabsState, closeOtherTabsState } from "@/lib/tabCloseActions";
|
||||
import { buildExplainSql, parseExplainResult } from "@/lib/explainPlan";
|
||||
import * as api from "@/lib/tauri";
|
||||
import * as api from "@/lib/api";
|
||||
|
||||
export const useQueryStore = defineStore("query", () => {
|
||||
const tabs = ref<QueryTab[]>([]);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
import * as api from "@/lib/tauri";
|
||||
import * as api from "@/lib/api";
|
||||
|
||||
export type AiProvider = "claude" | "openai" | "custom";
|
||||
export type AiApiStyle = "completions" | "responses";
|
||||
|
||||
Reference in New Issue
Block a user