refactor: split App.vue into composables and layout components

Extract 11 composables and 5 layout components from App.vue, reducing it
from 1921 to 327 lines. Unify dialog overlay style across DialogContent
and DialogScrollContent for consistent backdrop appearance.
This commit is contained in:
t8y2
2026-05-05 04:14:00 +08:00
parent 794c5375c7
commit bdd8b3fa03
22 changed files with 2217 additions and 1778 deletions
+114
View File
@@ -0,0 +1,114 @@
import { ref } from "vue";
import { useI18n } from "vue-i18n";
import { isTauriRuntime } from "@/lib/tauriRuntime";
import { useToast } from "@/composables/useToast";
import * as api from "@/lib/api";
export function useAppUpdater() {
const { t } = useI18n();
const { toast } = useToast();
const checkingUpdates = ref(false);
const updateInfo = ref<api.UpdateInfo | null>(null);
const updateCheckMessage = ref("");
const showUpdateDialog = ref(false);
const isDownloadingUpdate = ref(false);
const downloadProgress = ref(0);
const updateReady = ref(false);
const latestReleaseUrl = "https://github.com/t8y2/dbx/releases/latest";
function openUrl(url: string) {
if (isTauriRuntime()) {
import("@tauri-apps/plugin-shell").then(({ open }) => open(url));
} else {
window.open(url, "_blank");
}
}
async function checkUpdates(options: { silent?: boolean } = {}) {
if (checkingUpdates.value) return;
checkingUpdates.value = true;
updateCheckMessage.value = "";
try {
const info = await api.checkForUpdates();
updateInfo.value = info;
if (info.update_available) {
showUpdateDialog.value = true;
} else if (!options.silent) {
updateCheckMessage.value = t("updates.upToDate", { version: info.current_version });
showUpdateDialog.value = true;
}
} catch (e: any) {
if (!options.silent) {
updateCheckMessage.value = formatUpdateError(String(e));
showUpdateDialog.value = true;
}
} finally {
checkingUpdates.value = false;
}
}
function formatUpdateError(message: string): string {
const lower = message.toLowerCase();
if (lower.includes("403") || lower.includes("rate limit")) {
return t("updates.rateLimited");
}
return t("updates.failed", { error: message });
}
function openLatestRelease() {
const url = updateInfo.value?.release_url || latestReleaseUrl;
openUrl(url);
}
async function downloadAndInstallUpdate() {
if (!isTauriRuntime() || isDownloadingUpdate.value) return;
isDownloadingUpdate.value = true;
downloadProgress.value = 0;
try {
const { check } = await import("@tauri-apps/plugin-updater");
const update = await check();
if (!update) return;
let totalBytes = 0;
let downloadedBytes = 0;
await update.downloadAndInstall((event) => {
if (event.event === "Started" && event.data.contentLength) {
totalBytes = event.data.contentLength;
} else if (event.event === "Progress") {
downloadedBytes += event.data.chunkLength;
downloadProgress.value = totalBytes > 0 ? Math.round((downloadedBytes / totalBytes) * 100) : 0;
} else if (event.event === "Finished") {
downloadProgress.value = 100;
}
});
updateReady.value = true;
} catch (e: any) {
toast(t("updates.downloadFailed", { error: e?.message || String(e) }), 5000);
} finally {
isDownloadingUpdate.value = false;
}
}
async function restartApp() {
if (!isTauriRuntime()) return;
const { relaunch } = await import("@tauri-apps/plugin-process");
await relaunch();
}
return {
checkingUpdates,
updateInfo,
updateCheckMessage,
showUpdateDialog,
isDownloadingUpdate,
downloadProgress,
updateReady,
latestReleaseUrl,
openUrl,
checkUpdates,
formatUpdateError,
openLatestRelease,
downloadAndInstallUpdate,
restartApp,
};
}
+68
View File
@@ -0,0 +1,68 @@
import { type ComputedRef } from "vue";
import { useConnectionStore } from "@/stores/connectionStore";
import { useQueryStore } from "@/stores/queryStore";
import { buildTableSelectSql, quoteTableIdentifier } from "@/lib/tableSelectSql";
import type { QueryTab } from "@/types/database";
export function useDataGridActions(activeTab: ComputedRef<QueryTab | undefined>) {
const connectionStore = useConnectionStore();
const queryStore = useQueryStore();
function quoteIdent(tab: QueryTab, name: string): string {
const config = connectionStore.getConfig(tab.connectionId);
return quoteTableIdentifier(config?.db_type, name);
}
function buildTableSql(
tab: QueryTab,
options: { orderBy?: string; limit?: number; offset?: number; whereInput?: string } = {},
): string {
const config = connectionStore.getConfig(tab.connectionId);
const fallbackOrderColumns = config?.db_type === "sqlserver" && !tab.tableMeta?.primaryKeys?.length
? tab.tableMeta?.columns.slice(0, 1).map((column) => column.name)
: undefined;
return buildTableSelectSql({
databaseType: config?.db_type,
schema: tab.tableMeta?.schema,
tableName: tab.tableMeta?.tableName ?? "",
primaryKeys: tab.tableMeta?.primaryKeys,
fallbackOrderColumns,
...options,
});
}
async function onExecuteSql(sql: string) {
const tab = activeTab.value;
if (!tab) return;
queryStore.updateSql(tab.id, sql);
await queryStore.executeTabSql(tab.id, sql);
}
async function onReloadData() {
const tab = activeTab.value;
if (!tab) return;
if (tab.mode === "data" && tab.tableMeta) {
queryStore.updateSql(tab.id, buildTableSql(tab));
}
queryStore.executeCurrentTab();
}
async function onPaginate(offset: number, limit: number, whereInput?: string) {
const tab = activeTab.value;
if (!tab?.tableMeta) return;
const sql = buildTableSql(tab, { limit, offset, whereInput });
queryStore.updateSql(tab.id, sql);
await queryStore.executeCurrentTab();
}
async function onSort(column: string, direction: "asc" | "desc" | null, whereInput?: string) {
const tab = activeTab.value;
if (!tab?.tableMeta) return;
const orderBy = direction ? `${quoteIdent(tab, column)} ${direction.toUpperCase()}` : undefined;
const sql = buildTableSql(tab, { orderBy, whereInput });
queryStore.updateSql(tab.id, sql);
await queryStore.executeCurrentTab();
}
return { onExecuteSql, onReloadData, onPaginate, onSort };
}
+40
View File
@@ -0,0 +1,40 @@
import { ref } from "vue";
import { useConnectionStore } from "@/stores/connectionStore";
import * as api from "@/lib/api";
export function useDatabaseOptions() {
const connectionStore = useConnectionStore();
const databaseOptions = ref<Record<string, string[]>>({});
const loadingDatabaseOptions = ref<Record<string, boolean>>({});
async function loadDatabaseOptions(connectionId: string) {
const connection = connectionStore.getConfig(connectionId);
if (!connection || loadingDatabaseOptions.value[connectionId]) return;
loadingDatabaseOptions.value[connectionId] = true;
try {
await connectionStore.ensureConnected(connectionId);
if (connection.db_type === "redis") {
const dbs = await api.redisListDatabases(connectionId);
databaseOptions.value[connectionId] = dbs.map(String);
} else if (connection.db_type === "mongodb") {
databaseOptions.value[connectionId] = await api.mongoListDatabases(connectionId);
} else {
const dbs = await api.listDatabases(connectionId);
databaseOptions.value[connectionId] = dbs.map((db) => db.name);
}
} finally {
loadingDatabaseOptions.value[connectionId] = false;
}
}
async function getDatabaseOptions(connectionId: string): Promise<string[]> {
if (!databaseOptions.value[connectionId]) {
await loadDatabaseOptions(connectionId);
}
return databaseOptions.value[connectionId] ?? [];
}
return { databaseOptions, loadingDatabaseOptions, loadDatabaseOptions, getDatabaseOptions };
}
+242
View File
@@ -0,0 +1,242 @@
import { ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { useConnectionStore } from "@/stores/connectionStore";
import { useToast } from "@/composables/useToast";
import type { SidebarLayout } from "@/types/database";
const showTransferDialog = ref(false);
const showSchemaDiffDialog = ref(false);
const showSqlFileDialog = ref(false);
const showDiagramDialog = ref(false);
const showTableImportDialog = ref(false);
const showStructureEditorDialog = ref(false);
const showFieldLineageDialog = ref(false);
const showDatabaseSearchDialog = ref(false);
const showImportLayoutConfirm = ref(false);
const pendingImportLayout = ref<SidebarLayout | null>(null);
const showConfigPassphraseDialog = ref(false);
const configPassphraseMode = ref<"export" | "import">("export");
const configPassphraseError = ref("");
const pendingImportContent = ref("");
const transferPrefillConnectionId = ref("");
const transferPrefillDatabase = ref("");
const schemaDiffPrefillConnectionId = ref("");
const schemaDiffPrefillDatabase = ref("");
const sqlFilePrefillConnectionId = ref("");
const sqlFilePrefillDatabase = ref("");
const diagramPrefillConnectionId = ref("");
const diagramPrefillDatabase = ref("");
const diagramPrefillSchema = ref("");
const diagramFocusTableName = ref("");
const tableImportPrefillConnectionId = ref("");
const tableImportPrefillDatabase = ref("");
const tableImportPrefillSchema = ref("");
const tableImportPrefillTable = ref("");
const structurePrefillConnectionId = ref("");
const structurePrefillDatabase = ref("");
const structurePrefillSchema = ref("");
const structurePrefillTable = ref("");
const lineagePrefillConnectionId = ref("");
const lineagePrefillDatabase = ref("");
const lineagePrefillSchema = ref("");
const lineagePrefillTable = ref("");
const lineagePrefillColumn = ref("");
const databaseSearchPrefillConnectionId = ref("");
const databaseSearchPrefillDatabase = ref("");
const databaseSearchPrefillSchema = ref("");
let watchersRegistered = false;
export function useDialogSources() {
const { t } = useI18n();
const connectionStore = useConnectionStore();
const { toast } = useToast();
// Watchers for store source triggers (register only once)
if (!watchersRegistered) {
watchersRegistered = true;
watch(() => connectionStore.transferSource, (v) => {
if (v) {
transferPrefillConnectionId.value = v.connectionId;
transferPrefillDatabase.value = v.database;
showTransferDialog.value = true;
connectionStore.transferSource = null;
}
});
watch(() => connectionStore.schemaDiffSource, (v) => {
if (v) {
schemaDiffPrefillConnectionId.value = v.connectionId;
schemaDiffPrefillDatabase.value = v.database;
showSchemaDiffDialog.value = true;
connectionStore.schemaDiffSource = null;
}
});
watch(() => connectionStore.sqlFileSource, (v) => {
if (v) {
sqlFilePrefillConnectionId.value = v.connectionId;
sqlFilePrefillDatabase.value = v.database;
showSqlFileDialog.value = true;
connectionStore.sqlFileSource = null;
}
});
watch(() => connectionStore.diagramSource, (v) => {
if (v) {
diagramPrefillConnectionId.value = v.connectionId;
diagramPrefillDatabase.value = v.database;
diagramPrefillSchema.value = v.schema ?? "";
diagramFocusTableName.value = v.tableName ?? "";
showDiagramDialog.value = true;
connectionStore.diagramSource = null;
}
});
watch(() => connectionStore.tableImportSource, (v) => {
if (v) {
tableImportPrefillConnectionId.value = v.connectionId;
tableImportPrefillDatabase.value = v.database;
tableImportPrefillSchema.value = v.schema ?? "";
tableImportPrefillTable.value = v.tableName;
showTableImportDialog.value = true;
connectionStore.tableImportSource = null;
}
});
watch(() => connectionStore.structureEditorSource, (v) => {
if (v) {
structurePrefillConnectionId.value = v.connectionId;
structurePrefillDatabase.value = v.database;
structurePrefillSchema.value = v.schema ?? "";
structurePrefillTable.value = v.tableName;
showStructureEditorDialog.value = true;
connectionStore.structureEditorSource = null;
}
});
watch(() => connectionStore.fieldLineageSource, (v) => {
if (v) {
lineagePrefillConnectionId.value = v.connectionId;
lineagePrefillDatabase.value = v.database;
lineagePrefillSchema.value = v.schema ?? "";
lineagePrefillTable.value = v.tableName;
lineagePrefillColumn.value = v.columnName;
showFieldLineageDialog.value = true;
connectionStore.fieldLineageSource = null;
}
});
watch(() => connectionStore.databaseSearchSource, (v) => {
if (v) {
databaseSearchPrefillConnectionId.value = v.connectionId;
databaseSearchPrefillDatabase.value = v.database;
databaseSearchPrefillSchema.value = v.schema ?? "";
showDatabaseSearchDialog.value = true;
connectionStore.databaseSearchSource = null;
}
});
} // end watchersRegistered
// Config export/import helpers
function onExportClick() {
configPassphraseMode.value = "export";
configPassphraseError.value = "";
showConfigPassphraseDialog.value = true;
}
async function onExportConfirm(passphrase: string) {
try {
await connectionStore.exportConnectionsToFile(passphrase);
showConfigPassphraseDialog.value = false;
toast(t("configExport.exportSuccess"), 2000);
} catch (e: any) {
configPassphraseError.value = e?.message || String(e);
}
}
async function onImportClick() {
try {
const result = await connectionStore.readImportFile();
if (!result) return;
pendingImportContent.value = result.content;
if (result.encrypted) {
configPassphraseMode.value = "import";
configPassphraseError.value = "";
showConfigPassphraseDialog.value = true;
} else {
const { count, layout } = await connectionStore.importConnectionsFromFile(result.content, null);
toast(count > 0 ? t("configExport.importSuccess", { count }) : t("configExport.importNone"), 2000);
if (layout && count > 0) {
pendingImportLayout.value = layout;
showImportLayoutConfirm.value = true;
}
}
} catch (e: any) {
toast(e?.message || String(e), 4000);
}
}
async function onImportConfirm(passphrase: string) {
try {
const { count, layout } = await connectionStore.importConnectionsFromFile(pendingImportContent.value, passphrase);
showConfigPassphraseDialog.value = false;
toast(count > 0 ? t("configExport.importSuccess", { count }) : t("configExport.importNone"), 2000);
if (layout && count > 0) {
pendingImportLayout.value = layout;
showImportLayoutConfirm.value = true;
}
} catch (e: any) {
configPassphraseError.value = e?.message === "wrong_passphrase" ? t("configExport.wrongPassphrase") : (e?.message || String(e));
}
}
return {
showTransferDialog,
showSchemaDiffDialog,
showSqlFileDialog,
showDiagramDialog,
showTableImportDialog,
showStructureEditorDialog,
showFieldLineageDialog,
showDatabaseSearchDialog,
showImportLayoutConfirm,
pendingImportLayout,
showConfigPassphraseDialog,
configPassphraseMode,
configPassphraseError,
pendingImportContent,
transferPrefillConnectionId,
transferPrefillDatabase,
schemaDiffPrefillConnectionId,
schemaDiffPrefillDatabase,
sqlFilePrefillConnectionId,
sqlFilePrefillDatabase,
diagramPrefillConnectionId,
diagramPrefillDatabase,
diagramPrefillSchema,
diagramFocusTableName,
tableImportPrefillConnectionId,
tableImportPrefillDatabase,
tableImportPrefillSchema,
tableImportPrefillTable,
structurePrefillConnectionId,
structurePrefillDatabase,
structurePrefillSchema,
structurePrefillTable,
lineagePrefillConnectionId,
lineagePrefillDatabase,
lineagePrefillSchema,
lineagePrefillTable,
lineagePrefillColumn,
databaseSearchPrefillConnectionId,
databaseSearchPrefillDatabase,
databaseSearchPrefillSchema,
onExportClick,
onExportConfirm,
onImportClick,
onImportConfirm,
};
}
+90
View File
@@ -0,0 +1,90 @@
import { useI18n } from "vue-i18n";
import { useConnectionStore } from "@/stores/connectionStore";
import { useQueryStore } from "@/stores/queryStore";
import { useToast } from "@/composables/useToast";
import * as api from "@/lib/api";
import type { ConnectionConfig } from "@/types/database";
const DB_EXTENSIONS = [".db", ".sqlite", ".sqlite3", ".duckdb"];
function getDbType(path: string): "sqlite" | "duckdb" | null {
const lower = path.toLowerCase();
if (lower.endsWith(".duckdb")) return "duckdb";
if (DB_EXTENSIONS.some((ext) => lower.endsWith(ext))) return "sqlite";
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;
}
export function useFileDrop() {
const { t } = useI18n();
const connectionStore = useConnectionStore();
const queryStore = useQueryStore();
const { toast } = useToast();
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;
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 config: ConnectionConfig = {
id: crypto.randomUUID(),
name,
db_type: dbType,
driver_profile: dbType,
driver_label: dbType === "duckdb" ? "DuckDB" : "SQLite",
url_params: "",
host: path,
port: 0,
username: "",
password: "",
};
try {
await connectionStore.addConnection(config);
void connectionStore.connect(config);
toast(t("welcome.fileOpened", { name }));
} catch (e: any) {
toast(t("connection.saveFailed", { message: e?.message || String(e) }), 5000);
}
}
});
}
return { setupFileDrop };
}
+84
View File
@@ -0,0 +1,84 @@
import * as api from "@/lib/api";
import { buildTableSelectSql } from "@/lib/tableSelectSql";
import { useConnectionStore } from "@/stores/connectionStore";
import { useQueryStore } from "@/stores/queryStore";
export type NavigationTarget = {
connectionId: string;
database: string;
schema?: string;
tableName: string;
columnName?: string;
whereInput?: string;
};
async function openTableTarget(target: NavigationTarget) {
const connectionStore = useConnectionStore();
const queryStore = useQueryStore();
connectionStore.activeConnectionId = target.connectionId;
const config = connectionStore.getConfig(target.connectionId);
const tabTitle = target.schema ? `${target.schema}.${target.tableName}` : target.tableName;
const tabId = queryStore.createTab(target.connectionId, target.database, tabTitle, "data");
queryStore.setExecuting(tabId, true);
try {
await connectionStore.ensureConnected(target.connectionId);
if (!config) throw new Error("Connection config not found");
const querySchema = target.schema || target.database;
const columns = await api.getColumns(target.connectionId, target.database, querySchema, target.tableName);
const primaryKeys = columns.filter((c) => c.is_primary_key).map((c) => c.name);
const sql = buildTableSelectSql({
databaseType: config.db_type,
schema: target.schema,
tableName: target.tableName,
primaryKeys,
whereInput: target.whereInput,
});
queryStore.updateSql(tabId, sql);
queryStore.setTableMeta(tabId, { schema: target.schema, tableName: target.tableName, columns, primaryKeys });
await queryStore.executeTabSql(tabId, sql);
} catch (e: any) {
queryStore.setErrorResult(tabId, e);
}
}
export function useNavigationTargets(dialogs: {
showFieldLineageDialog: { value: boolean };
showDatabaseSearchDialog: { value: boolean };
structurePrefillTable: { value: string };
}) {
const queryStore = useQueryStore();
async function openLineageTarget(target: NavigationTarget) {
dialogs.showFieldLineageDialog.value = false;
await openTableTarget(target);
}
async function openDatabaseSearchTarget(target: NavigationTarget) {
dialogs.showDatabaseSearchDialog.value = false;
await openTableTarget(target);
}
async function onStructureEditorSaved(reloadData: () => Promise<void>, toast: (msg: string, duration?: number) => void) {
const activeTab = queryStore.tabs.find((t) => t.id === queryStore.activeTabId);
if (activeTab?.mode === "data" && activeTab.tableMeta?.tableName === dialogs.structurePrefillTable.value) {
try {
const columns = await api.getColumns(
activeTab.connectionId, activeTab.database,
activeTab.tableMeta.schema || activeTab.database, activeTab.tableMeta.tableName,
);
queryStore.setTableMeta(activeTab.id, {
...activeTab.tableMeta,
columns,
primaryKeys: columns.filter((c) => c.is_primary_key).map((c) => c.name),
});
await reloadData();
} catch (e: any) {
toast(e?.message || String(e), 5000);
}
}
}
return { openLineageTarget, openDatabaseSearchTarget, onStructureEditorSaved, openTableTarget };
}
+42
View File
@@ -0,0 +1,42 @@
import { ref, type Ref } from "vue";
export function usePanelResize() {
const sidebarWidth = ref(Number(localStorage.getItem("dbx-sidebar-width")) || 260);
const aiPanelWidth = ref(Number(localStorage.getItem("dbx-ai-panel-width")) || 360);
const historyWidth = ref(Number(localStorage.getItem("dbx-history-width")) || 288);
function startPanelResize(widthRef: Ref<number>, storageKey: string, direction: 'left' | 'right') {
return (e: MouseEvent) => {
e.preventDefault();
const startX = e.clientX;
const startWidth = widthRef.value;
const onMouseMove = (ev: MouseEvent) => {
const delta = ev.clientX - startX;
widthRef.value = Math.max(180, Math.min(800, startWidth + (direction === 'right' ? delta : -delta)));
};
const onMouseUp = () => {
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
localStorage.setItem(storageKey, String(widthRef.value));
};
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
};
}
const startSidebarResize = startPanelResize(sidebarWidth, "dbx-sidebar-width", 'right');
const startAiPanelResize = startPanelResize(aiPanelWidth, "dbx-ai-panel-width", 'left');
const startHistoryResize = startPanelResize(historyWidth, "dbx-history-width", 'left');
return {
sidebarWidth,
aiPanelWidth,
historyWidth,
startSidebarResize,
startAiPanelResize,
startHistoryResize,
};
}
+118
View File
@@ -0,0 +1,118 @@
import { ref, type Ref, type ComputedRef } from "vue";
import { useI18n } from "vue-i18n";
import { useQueryStore } from "@/stores/queryStore";
import { useHistoryStore } from "@/stores/historyStore";
import { useConnectionStore } from "@/stores/connectionStore";
import { useToast } from "@/composables/useToast";
import type { ConnectionConfig, QueryTab } from "@/types/database";
const DANGER_RE = /\b(DROP|DELETE|TRUNCATE|ALTER|UPDATE|MERGE|REPLACE)\b/i;
export function stripSqlComments(sql: string): string {
return sql
.replace(/\/\*[\s\S]*?\*\//g, " ")
.replace(/--.*$/gm, " ")
.replace(/#.*$/gm, " ");
}
export function isDangerousSql(sql: string): boolean {
return DANGER_RE.test(stripSqlComments(sql));
}
export function useSqlExecution(deps: {
activeTab: ComputedRef<QueryTab | undefined>;
activeConnection: ComputedRef<ConnectionConfig | undefined>;
executableSql: ComputedRef<string>;
activeOutputView: Ref<"result" | "explain">;
}) {
const { t } = useI18n();
const queryStore = useQueryStore();
const historyStore = useHistoryStore();
const connectionStore = useConnectionStore();
const { toast } = useToast();
const dangerSql = ref("");
const pendingDangerSql = ref("");
const showDangerDialog = ref(false);
function tryExecute(sqlOverride?: string) {
const tab = deps.activeTab.value;
const sql = sqlOverride ?? deps.executableSql.value;
if (!tab || !sql.trim()) return;
if (isDangerousSql(sql)) {
dangerSql.value = sql;
pendingDangerSql.value = sql;
showDangerDialog.value = true;
} else {
doExecute(sql);
}
}
async function doExecute(sql = deps.executableSql.value) {
const tab = deps.activeTab.value;
if (!tab || !sql.trim()) return;
deps.activeOutputView.value = "result";
const connName = connectionStore.getConfig(tab.connectionId)?.name || "";
const start = Date.now();
await queryStore.executeCurrentSql(sql);
const elapsed = Date.now() - start;
const success = !tab.result?.columns.includes("Error");
historyStore.add({
connection_name: connName,
database: tab.database,
sql,
execution_time_ms: elapsed,
success,
error: success ? undefined : String(tab.result?.rows?.[0]?.[0] ?? ""),
});
}
function cancelActiveExecution() {
const tab = deps.activeTab.value;
if (!tab) return;
if (tab.isExecuting) void queryStore.cancelTabExecution(tab.id);
else if (tab.isExplaining) void queryStore.cancelTabExplain(tab.id);
}
function explainReasonMessage(reason: string): string {
if (reason === "unsupported") return t("explain.unsupported");
if (reason === "unsafe") return t("explain.unsafe");
return t("explain.emptySql");
}
async function tryExplain(sqlOverride?: string) {
const tab = deps.activeTab.value;
const sql = sqlOverride ?? deps.executableSql.value;
if (!tab || !sql.trim()) {
toast(t("explain.emptySql"));
return;
}
deps.activeOutputView.value = "explain";
const result = await queryStore.explainTabSql(tab.id, sql, deps.activeConnection.value?.db_type);
if (!result.ok) {
toast(explainReasonMessage(result.reason), 5000);
return;
}
const current = deps.activeTab.value;
if (current?.explainError) toast(current.explainError, 5000);
}
function onDangerConfirm() {
const sql = pendingDangerSql.value || deps.executableSql.value;
pendingDangerSql.value = "";
doExecute(sql);
}
return {
dangerSql,
pendingDangerSql,
showDangerDialog,
tryExecute,
doExecute,
cancelActiveExecution,
tryExplain,
onDangerConfirm,
};
}
+26
View File
@@ -0,0 +1,26 @@
import { ref, type Ref } from "vue";
export function useTabScroll(tabsContainerRef: Ref<HTMLElement | null>) {
const canScrollLeft = ref(false);
const canScrollRight = ref(false);
function updateScrollButtons() {
const el = tabsContainerRef.value;
if (!el) {
canScrollLeft.value = false;
canScrollRight.value = false;
return;
}
canScrollLeft.value = el.scrollLeft > 0;
canScrollRight.value = el.scrollLeft < el.scrollWidth - el.clientWidth - 1;
}
function scrollTabs(direction: "left" | "right") {
const el = tabsContainerRef.value;
if (!el) return;
const scrollAmount = el.clientWidth * 0.8;
el.scrollBy({ left: direction === "left" ? -scrollAmount : scrollAmount, behavior: "smooth" });
}
return { canScrollLeft, canScrollRight, updateScrollButtons, scrollTabs };
}
+55
View File
@@ -0,0 +1,55 @@
import { useConnectionStore } from "@/stores/connectionStore";
import { useQueryStore } from "@/stores/queryStore";
import type { NavigationTarget } from "@/composables/useNavigationTargets";
export function useTauriEvents(deps: {
openTableTarget: (target: NavigationTarget) => Promise<void>;
}) {
const connectionStore = useConnectionStore();
const queryStore = useQueryStore();
function setupTauriListeners() {
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;
if (!connectionStore.connections.length) await connectionStore.initFromDisk();
const config = connectionStore.getConfig(connection_id);
if (!config) return;
connectionStore.activeConnectionId = connection_id;
await connectionStore.ensureConnected(connection_id);
if (config.db_type === "redis") {
queryStore.createTab(connection_id, database || "0", `db${database || "0"}`, "redis");
} else if (config.db_type === "mongodb") {
queryStore.createTab(connection_id, database, table, "mongo");
} else {
deps.openTableTarget({ connectionId: connection_id, database, schema, tableName: table });
}
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;
if (!connectionStore.connections.length) await connectionStore.initFromDisk();
const config = connectionStore.getConfig(connection_id);
if (!config) return;
connectionStore.activeConnectionId = connection_id;
await connectionStore.ensureConnected(connection_id);
const tabId = queryStore.createTab(connection_id, database, undefined, "query");
queryStore.updateSql(tabId, sql);
await queryStore.executeTabSql(tabId, sql);
import("@tauri-apps/api/window").then(({ getCurrentWindow }) =>
getCurrentWindow().setFocus().catch(() => {}),
);
},
);
}).catch(() => {});
}
return { setupTauriListeners };
}
+25
View File
@@ -0,0 +1,25 @@
import { ref } from "vue";
import { isTauriRuntime } from "@/lib/tauriRuntime";
import type { Theme } from "@tauri-apps/api/window";
export function useTheme() {
const isDark = ref(localStorage.getItem("dbx-theme") === "dark");
function applyTheme() {
document.documentElement.classList.toggle("dark", isDark.value);
if (!isTauriRuntime()) return;
import("@tauri-apps/api/window").then(({ getCurrentWindow }) => {
getCurrentWindow()
.setTheme(isDark.value ? "dark" as Theme : "light" as Theme)
.catch(() => {});
});
}
function toggleTheme() {
isDark.value = !isDark.value;
localStorage.setItem("dbx-theme", isDark.value ? "dark" : "light");
applyTheme();
}
return { isDark, applyTheme, toggleTheme };
}