mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-31 16:24:33 +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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user