Merge remote-tracking branch 'upstream/main' into codex/fix-260-482-overflow-navigation
# Conflicts: # apps/desktop/src/components/layout/AppTabBar.vue # apps/desktop/src/lib/tabPresentation.ts
This commit is contained in:
commit
e36e42723a
52 files changed
+2915
-1044
No files matched your search
@@ -4,6 +4,7 @@ import { useI18n } from "vue-i18n";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Lock, Loader2, ShieldCheck } from "lucide-vue-next";
|
||||
import AppLogo from "@/components/icons/AppLogo.vue";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -55,7 +56,7 @@ async function submit() {
|
||||
>
|
||||
<div class="w-[360px] space-y-8">
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<img src="/logo.png" alt="DBX" class="w-20 h-20 rounded-2xl shadow-lg shadow-blue-500/20" />
|
||||
<AppLogo class="w-20 h-20 rounded-2xl shadow-lg shadow-blue-500/20" />
|
||||
<div class="text-center">
|
||||
<h1 class="text-2xl font-bold tracking-tight">DBX</h1>
|
||||
<p class="text-sm text-muted-foreground mt-1">
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
type AiProvider,
|
||||
type AiApiStyle,
|
||||
type EditorTheme,
|
||||
type DesktopIconTheme,
|
||||
} from "@/stores/settingsStore";
|
||||
import { loadEditorTheme, editorFontTheme } from "@/lib/editorThemes";
|
||||
import { isTauriRuntime } from "@/lib/tauriRuntime";
|
||||
@@ -66,6 +67,7 @@ import type { SqlSnippet } from "@/types/database";
|
||||
import { uuid } from "@/lib/utils";
|
||||
import { DEFAULT_SQL_SNIPPETS } from "@/lib/sqlCompletion";
|
||||
import AiProviderLogo from "@/components/icons/AiProviderLogo.vue";
|
||||
import AppLogo from "@/components/icons/AppLogo.vue";
|
||||
import type { AppThemeAppearance } from "@/lib/appTheme";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
|
||||
@@ -93,6 +95,7 @@ const editExecuteMode = ref(settingsStore.editorSettings.executeMode);
|
||||
const editWordWrap = ref(settingsStore.editorSettings.wordWrap);
|
||||
const editAppLayout = ref(settingsStore.editorSettings.appLayout);
|
||||
const editShowTrayIcon = ref(settingsStore.desktopSettings.show_tray_icon);
|
||||
const editIconTheme = ref<DesktopIconTheme>(settingsStore.desktopSettings.icon_theme);
|
||||
const editShowColumnCommentsInHeader = ref(settingsStore.editorSettings.showColumnCommentsInHeader);
|
||||
const editCompactColumnHeaderActions = ref(settingsStore.editorSettings.compactColumnHeaderActions);
|
||||
const editRedisScanPageSize = ref(settingsStore.editorSettings.redisScanPageSize);
|
||||
@@ -229,6 +232,7 @@ watch(
|
||||
editWordWrap.value = settingsStore.editorSettings.wordWrap;
|
||||
editAppLayout.value = settingsStore.editorSettings.appLayout;
|
||||
editShowTrayIcon.value = settingsStore.desktopSettings.show_tray_icon;
|
||||
editIconTheme.value = settingsStore.desktopSettings.icon_theme;
|
||||
editShowColumnCommentsInHeader.value = settingsStore.editorSettings.showColumnCommentsInHeader;
|
||||
editCompactColumnHeaderActions.value = settingsStore.editorSettings.compactColumnHeaderActions;
|
||||
editRedisScanPageSize.value = settingsStore.editorSettings.redisScanPageSize;
|
||||
@@ -268,6 +272,7 @@ function hasChanges(): boolean {
|
||||
editWordWrap.value !== settingsStore.editorSettings.wordWrap ||
|
||||
editAppLayout.value !== settingsStore.editorSettings.appLayout ||
|
||||
editShowTrayIcon.value !== settingsStore.desktopSettings.show_tray_icon ||
|
||||
editIconTheme.value !== settingsStore.desktopSettings.icon_theme ||
|
||||
editShowColumnCommentsInHeader.value !== settingsStore.editorSettings.showColumnCommentsInHeader ||
|
||||
editCompactColumnHeaderActions.value !== settingsStore.editorSettings.compactColumnHeaderActions ||
|
||||
editRedisScanPageSize.value !== settingsStore.editorSettings.redisScanPageSize ||
|
||||
@@ -309,6 +314,7 @@ async function persistSettings() {
|
||||
});
|
||||
await settingsStore.updateDesktopSettings({
|
||||
show_tray_icon: editShowTrayIcon.value,
|
||||
icon_theme: editIconTheme.value,
|
||||
});
|
||||
if (sidebarObjectDisplayChanged) {
|
||||
await connectionStore.refreshAllTree();
|
||||
@@ -333,6 +339,7 @@ function resetDefaults() {
|
||||
editWordWrap.value = DEFAULT_EDITOR_SETTINGS.wordWrap;
|
||||
editAppLayout.value = DEFAULT_EDITOR_SETTINGS.appLayout;
|
||||
editShowTrayIcon.value = DEFAULT_DESKTOP_SETTINGS.show_tray_icon;
|
||||
editIconTheme.value = DEFAULT_DESKTOP_SETTINGS.icon_theme;
|
||||
editShowColumnCommentsInHeader.value = DEFAULT_EDITOR_SETTINGS.showColumnCommentsInHeader;
|
||||
editCompactColumnHeaderActions.value = DEFAULT_EDITOR_SETTINGS.compactColumnHeaderActions;
|
||||
editRedisScanPageSize.value = DEFAULT_EDITOR_SETTINGS.redisScanPageSize;
|
||||
@@ -367,6 +374,10 @@ function setSidebarObjectDisplay(value: "grouped" | "simple") {
|
||||
editSidebarObjectDisplay.value = value;
|
||||
}
|
||||
|
||||
function setIconTheme(value: DesktopIconTheme) {
|
||||
editIconTheme.value = value;
|
||||
}
|
||||
|
||||
function onShortcutChange(actionId: ShortcutActionId, value: any) {
|
||||
if (typeof value !== "string") return;
|
||||
const definition = SHORTCUT_DEFINITIONS.find((item) => item.id === actionId);
|
||||
@@ -596,6 +607,7 @@ watch(
|
||||
await settingsStore.initAiConfig();
|
||||
await settingsStore.initDesktopSettings();
|
||||
editShowTrayIcon.value = settingsStore.desktopSettings.show_tray_icon;
|
||||
editIconTheme.value = settingsStore.desktopSettings.icon_theme;
|
||||
webdavPassword.value = "";
|
||||
await refreshWebDavPasswordStatus();
|
||||
syncAiEditState();
|
||||
@@ -1169,6 +1181,43 @@ watch(
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!isWeb" class="space-y-2">
|
||||
<Label>{{ t("settings.iconTheme") }}</Label>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
class="h-auto justify-start border p-3"
|
||||
:class="editIconTheme === 'default' ? 'border-blue-300 ring-2 ring-blue-300/50' : ''"
|
||||
@click="setIconTheme('default')"
|
||||
>
|
||||
<div class="flex items-center gap-3 text-left">
|
||||
<img src="/logo.png" alt="DBX" class="h-8 w-8 rounded-md" />
|
||||
<div>
|
||||
<div class="text-sm font-medium">{{ t("settings.iconThemeDefault") }}</div>
|
||||
<div class="text-xs text-muted-foreground">{{ t("settings.iconThemeDefaultDescription") }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
class="h-auto justify-start border p-3"
|
||||
:class="editIconTheme === 'black' ? 'border-blue-300 ring-2 ring-blue-300/50' : ''"
|
||||
@click="setIconTheme('black')"
|
||||
>
|
||||
<div class="flex items-center gap-3 text-left">
|
||||
<img src="/logo-black.png" alt="DBX" class="h-8 w-8 dark:invert" />
|
||||
<div>
|
||||
<div class="text-sm font-medium">{{ t("settings.iconThemeBlack") }}</div>
|
||||
<div class="text-xs text-muted-foreground">{{ t("settings.iconThemeBlackDescription") }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!isWeb"
|
||||
class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2"
|
||||
@@ -1910,7 +1959,7 @@ watch(
|
||||
{{ t("settings.project") }}
|
||||
</div>
|
||||
<div class="mt-3 flex items-center gap-2 text-sm font-medium">
|
||||
<img src="/logo.png" alt="DBX" class="h-7 w-7 rounded-md" />
|
||||
<AppLogo class="h-7 w-7" />
|
||||
{{ t("settings.officialDocs") }}
|
||||
<ExternalLink class="ml-auto h-3.5 w-3.5 text-muted-foreground" />
|
||||
</div>
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -110,6 +110,16 @@ function stepTime(part: "hour" | "minute" | "second", delta: number) {
|
||||
updateTime(part, (current + delta + max + 1) % (max + 1));
|
||||
}
|
||||
|
||||
function flushInputValue(target: EventTarget | null) {
|
||||
if (!(target instanceof HTMLInputElement)) return;
|
||||
const part = target.dataset.temporalPart;
|
||||
if (part === "year" || part === "month" || part === "day") {
|
||||
updateDate(part, target.value);
|
||||
} else if (part === "hour" || part === "minute" || part === "second") {
|
||||
updateTime(part, target.value);
|
||||
}
|
||||
}
|
||||
|
||||
function setNull() {
|
||||
setModelValue("NULL");
|
||||
}
|
||||
@@ -144,6 +154,7 @@ function finishCancel() {
|
||||
function onKeydown(event: KeyboardEvent) {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
flushInputValue(event.target);
|
||||
finishCommit();
|
||||
} else if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
@@ -208,6 +219,7 @@ function twoDigit(value: string | number): string {
|
||||
>
|
||||
<input
|
||||
:value="dateParts.year"
|
||||
data-temporal-part="year"
|
||||
inputmode="numeric"
|
||||
class="min-w-0 bg-transparent px-1 text-center text-[13px] tabular-nums outline-none"
|
||||
@change="updateDateFromInput('year', $event)"
|
||||
@@ -231,6 +243,7 @@ function twoDigit(value: string | number): string {
|
||||
>
|
||||
<input
|
||||
:value="twoDigit(dateParts.month)"
|
||||
data-temporal-part="month"
|
||||
inputmode="numeric"
|
||||
class="min-w-0 bg-transparent px-1 text-center text-[13px] tabular-nums outline-none"
|
||||
@change="updateDateFromInput('month', $event)"
|
||||
@@ -254,6 +267,7 @@ function twoDigit(value: string | number): string {
|
||||
>
|
||||
<input
|
||||
:value="twoDigit(dateParts.day)"
|
||||
data-temporal-part="day"
|
||||
inputmode="numeric"
|
||||
class="min-w-0 bg-transparent px-1 text-center text-[13px] tabular-nums outline-none"
|
||||
@change="updateDateFromInput('day', $event)"
|
||||
@@ -279,6 +293,7 @@ function twoDigit(value: string | number): string {
|
||||
>
|
||||
<input
|
||||
:value="twoDigit(timeParts.hour)"
|
||||
data-temporal-part="hour"
|
||||
inputmode="numeric"
|
||||
class="min-w-0 bg-transparent px-1 text-center text-[13px] tabular-nums outline-none"
|
||||
@change="updateTimeFromInput('hour', $event)"
|
||||
@@ -302,6 +317,7 @@ function twoDigit(value: string | number): string {
|
||||
>
|
||||
<input
|
||||
:value="twoDigit(timeParts.minute)"
|
||||
data-temporal-part="minute"
|
||||
inputmode="numeric"
|
||||
class="min-w-0 bg-transparent px-1 text-center text-[13px] tabular-nums outline-none"
|
||||
@change="updateTimeFromInput('minute', $event)"
|
||||
@@ -329,6 +345,7 @@ function twoDigit(value: string | number): string {
|
||||
>
|
||||
<input
|
||||
:value="twoDigit(timeParts.second)"
|
||||
data-temporal-part="second"
|
||||
inputmode="numeric"
|
||||
class="min-w-0 bg-transparent px-1 text-center text-[13px] tabular-nums outline-none"
|
||||
@change="updateTimeFromInput('second', $event)"
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
const isBlackLogo = computed(() => settingsStore.desktopSettings.icon_theme === "black");
|
||||
const logoSrc = computed(() => (isBlackLogo.value ? "/logo-black.png" : "/logo.png"));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<img :src="logoSrc" alt="DBX" :class="isBlackLogo ? 'dark:invert' : ''" />
|
||||
</template>
|
||||
@@ -18,11 +18,15 @@ import {
|
||||
import CustomContextMenu, { type ContextMenuItem } from "@/components/ui/CustomContextMenu.vue";
|
||||
import LightDropdown from "@/components/ui/LightDropdown.vue";
|
||||
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { useQueryStore } from "@/stores/queryStore";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import { useTabScroll } from "@/composables/useTabScroll";
|
||||
import { connectionColor, shouldShowTabOverflowControls } from "@/lib/tabPresentation";
|
||||
import {
|
||||
connectionColor,
|
||||
shouldShowTabOverflowControls,
|
||||
tabDisplayTitle,
|
||||
tabTooltipLines,
|
||||
} from "@/lib/tabPresentation";
|
||||
import { hexToRgba } from "@/lib/color";
|
||||
import type { QueryTab } from "@/types/database";
|
||||
|
||||
@@ -37,7 +41,6 @@ const emit = defineEmits<{
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
const connectionStore = useConnectionStore();
|
||||
const queryStore = useQueryStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
const compactTabTitle = computed({
|
||||
@@ -170,83 +173,11 @@ const showTabOverflowControls = computed(() =>
|
||||
),
|
||||
);
|
||||
|
||||
const connectionSummariesById = computed(
|
||||
() =>
|
||||
new Map(
|
||||
connectionStore.connections.map((connection) => [
|
||||
connection.id,
|
||||
{ name: connection.name, databaseType: connection.db_type },
|
||||
]),
|
||||
),
|
||||
);
|
||||
|
||||
function connectionNameForTab(tab: Pick<QueryTab, "connectionId">): string {
|
||||
return connectionSummariesById.value.get(tab.connectionId)?.name || tab.connectionId;
|
||||
}
|
||||
|
||||
function databaseNameForTab(tab: Pick<QueryTab, "connectionId" | "database">): string {
|
||||
const connection = connectionSummariesById.value.get(tab.connectionId);
|
||||
if (connection?.databaseType === "redis" && tab.database !== "") return `db${tab.database}`;
|
||||
return tab.database || t("editor.noDatabase");
|
||||
}
|
||||
|
||||
function isPreviewTab(tab: Pick<QueryTab, "connectionId">): boolean {
|
||||
return !!connectionSummariesById.value.get(tab.connectionId)?.name.startsWith("[Preview]");
|
||||
}
|
||||
|
||||
function tabTitle(tab: QueryTab): string {
|
||||
const database = databaseNameForTab(tab);
|
||||
if (isPreviewTab(tab)) return tab.title;
|
||||
if (tab.mode === "data" && tab.tableMeta?.tableName) {
|
||||
if (compactTabTitle.value) return tab.tableMeta.tableName;
|
||||
const suffix =
|
||||
tab.tableMeta.schema && tab.tableMeta.schema !== tab.database
|
||||
? `@${database}.${tab.tableMeta.schema}`
|
||||
: `@${database}`;
|
||||
return `${tab.tableMeta.tableName}${suffix}`;
|
||||
}
|
||||
if (tab.mode === "query") {
|
||||
if (compactTabTitle.value) return connectionNameForTab(tab);
|
||||
return `${connectionNameForTab(tab)}@${database}`;
|
||||
}
|
||||
if (tab.mode === "mongo" && tab.sql) {
|
||||
if (compactTabTitle.value) return tab.sql;
|
||||
return `${tab.sql}@${database}`;
|
||||
}
|
||||
if (tab.mode === "redis") {
|
||||
if (compactTabTitle.value) return connectionNameForTab(tab);
|
||||
return `${connectionNameForTab(tab)}@${database}`;
|
||||
}
|
||||
if (tab.mode === "objects") {
|
||||
const schema = tab.objectBrowser?.schema;
|
||||
if (compactTabTitle.value) return schema || tab.title;
|
||||
return schema ? `${schema}@${database}` : `${tab.title}@${database}`;
|
||||
}
|
||||
return tab.title;
|
||||
}
|
||||
|
||||
function tabTooltipRows(tab: QueryTab): { label: string; value: string }[] {
|
||||
const rows: { label: string; value: string }[] = [
|
||||
{ label: t("tabs.tooltipConnection"), value: connectionNameForTab(tab) },
|
||||
{ label: t("tabs.tooltipDatabase"), value: databaseNameForTab(tab) },
|
||||
];
|
||||
if (tab.mode === "data" && tab.tableMeta?.tableName) {
|
||||
rows.push({ label: t("tabs.tooltipTable"), value: tab.tableMeta.tableName });
|
||||
}
|
||||
if (tab.mode === "mongo" && tab.sql) {
|
||||
rows.push({ label: t("tabs.tooltipCollection"), value: tab.sql });
|
||||
}
|
||||
if (tab.mode === "objects" && tab.objectBrowser?.schema) {
|
||||
rows.push({ label: t("tabs.tooltipSchema"), value: tab.objectBrowser.schema });
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
const openTabMenuItems = computed(() =>
|
||||
queryStore.tabs.map((tab) => ({
|
||||
value: tab.id,
|
||||
label: tabTitle(tab),
|
||||
title: tabTitle(tab),
|
||||
label: tabDisplayTitle(tab, t),
|
||||
title: tabDisplayTitle(tab, t),
|
||||
icon: tabMenuIcon(tab),
|
||||
iconClass: tabIconClass(tab),
|
||||
})),
|
||||
@@ -354,7 +285,7 @@ const tabOverflowControlClass = computed(() =>
|
||||
<PencilRuler v-else-if="tab.mode === 'structure'" class="h-3.5 w-3.5" />
|
||||
<Code2 v-else class="h-3.5 w-3.5" />
|
||||
</span>
|
||||
<span class="min-w-0 truncate flex-1">{{ tabTitle(tab) }}</span>
|
||||
<span class="min-w-0 truncate flex-1">{{ tabDisplayTitle(tab, t) }}</span>
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<button
|
||||
@@ -376,7 +307,7 @@ const tabOverflowControlClass = computed(() =>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" class="text-xs grid grid-cols-[auto_1fr] gap-x-2">
|
||||
<template v-for="line in tabTooltipRows(tab)" :key="line.label">
|
||||
<template v-for="line in tabTooltipLines(tab, t)" :key="line.label">
|
||||
<span class="text-muted-foreground">{{ line.label }}</span>
|
||||
<span>{{ line.value }}</span>
|
||||
</template>
|
||||
|
||||
@@ -22,7 +22,13 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover
|
||||
import QueryEditor from "@/components/editor/QueryEditor.vue";
|
||||
import ColumnInfoPanel from "@/components/editor/ColumnInfoPanel.vue";
|
||||
import type { ColumnInfo } from "@/components/editor/ColumnInfoPanel.vue";
|
||||
const DataGrid = defineAsyncComponent(() => import("@/components/grid/DataGrid.vue"));
|
||||
const DataGrid = defineAsyncComponent(async () => {
|
||||
const startedAt = performance.now();
|
||||
console.info("[DBX][DataGrid:load:start]");
|
||||
const component = await import("@/components/grid/DataGrid.vue");
|
||||
console.info("[DBX][DataGrid:load:done]", { elapsed: `${Math.round(performance.now() - startedAt)}ms` });
|
||||
return component;
|
||||
});
|
||||
const RedisKeyBrowser = defineAsyncComponent(() => import("@/components/redis/RedisKeyBrowser.vue"));
|
||||
const MongoDocBrowser = defineAsyncComponent(() => import("@/components/mongo/MongoDocBrowser.vue"));
|
||||
const ObjectBrowser = defineAsyncComponent(() => import("@/components/objects/ObjectBrowser.vue"));
|
||||
@@ -171,6 +177,35 @@ watch(
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.activeTab.result,
|
||||
(result) => {
|
||||
if (!result) return;
|
||||
const startedAt = performance.now();
|
||||
console.info("[DBX][ContentArea:result:observed]", {
|
||||
tabId: props.activeTab.id,
|
||||
rowCount: result.rows.length,
|
||||
columnCount: result.columns.length,
|
||||
backendMs: result.execution_time_ms,
|
||||
isExecuting: props.activeTab.isExecuting,
|
||||
});
|
||||
nextTick(() => {
|
||||
console.info("[DBX][ContentArea:result:nextTick]", {
|
||||
tabId: props.activeTab.id,
|
||||
elapsed: `${Math.round(performance.now() - startedAt)}ms`,
|
||||
isExecuting: props.activeTab.isExecuting,
|
||||
});
|
||||
requestAnimationFrame(() => {
|
||||
console.info("[DBX][ContentArea:result:first-frame]", {
|
||||
tabId: props.activeTab.id,
|
||||
elapsed: `${Math.round(performance.now() - startedAt)}ms`,
|
||||
isExecuting: props.activeTab.isExecuting,
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.activeTab.isExecuting,
|
||||
(isExecuting, wasExecuting) => {
|
||||
@@ -445,19 +480,19 @@ defineExpose({ focusSearch, refreshData, handleModRTarget });
|
||||
(column: string, columnIndex: number, direction: 'asc' | 'desc' | null, whereInput?: string) =>
|
||||
emit('sort', column, columnIndex, direction, whereInput)
|
||||
"
|
||||
/>
|
||||
<div
|
||||
v-if="activeTab.result?.columns.includes('Error')"
|
||||
class="flex items-center gap-2 px-3 py-1.5 border-t bg-destructive/5"
|
||||
>
|
||||
<Bot class="h-3.5 w-3.5 text-destructive" />
|
||||
<button
|
||||
class="text-xs text-destructive hover:underline"
|
||||
@click="emit('fixWithAi', String(activeTab.result?.rows?.[0]?.[0] ?? ''))"
|
||||
>
|
||||
{{ t("ai.fixWithAi") }}
|
||||
</button>
|
||||
</div>
|
||||
<template v-if="activeTab.result?.columns.includes('Error')" #error-actions="{ errorMessage }">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="mt-2 h-7 gap-1.5 border-destructive/30 bg-background px-2.5 text-xs text-destructive hover:bg-destructive/10 hover:text-destructive"
|
||||
@click="emit('fixWithAi', String(errorMessage))"
|
||||
>
|
||||
<Bot class="h-3.5 w-3.5" />
|
||||
{{ t("ai.fixWithAi") }}
|
||||
</Button>
|
||||
</template>
|
||||
</DataGrid>
|
||||
<div
|
||||
v-else-if="!activeTab.result && activeTab.isExecuting"
|
||||
class="flex-1 min-h-0 flex flex-col items-center justify-center gap-3 text-muted-foreground text-sm"
|
||||
@@ -499,7 +534,7 @@ defineExpose({ focusSearch, refreshData, handleModRTarget });
|
||||
class="inline-flex items-center rounded border border-border bg-muted/30 px-2 py-0.5 text-muted-foreground truncate"
|
||||
>
|
||||
<template v-if="activeTab.tableMeta?.schema">{{ activeTab.tableMeta.schema }}@</template
|
||||
>{{ databaseDisplayNameForTab(activeTab.connectionId, activeTab.database) }}
|
||||
>{{ databaseDisplayNameForTab(activeTab.connectionId, activeTab.database, t) }}
|
||||
</span>
|
||||
<span v-if="activeTab.tableMeta" class="ml-auto text-muted-foreground">
|
||||
{{ activeTab.tableMeta.columns.length }} {{ t("tree.columns") }}
|
||||
|
||||
@@ -65,7 +65,7 @@ import {
|
||||
setActiveTableReferencePayload,
|
||||
type QueryEditorTableReferencePayload,
|
||||
} from "@/lib/queryEditorTableDrop";
|
||||
import { editablePrimaryKeys, usesSyntheticRowIdKey } from "@/lib/tableEditing";
|
||||
import { editablePrimaryKeys } from "@/lib/tableEditing";
|
||||
import {
|
||||
supportsDatabaseCreation,
|
||||
supportsDatabaseSearch,
|
||||
@@ -531,49 +531,57 @@ async function openData() {
|
||||
if (!config) throw new Error("Connection config not found");
|
||||
|
||||
const querySchema = node.schema || node.database;
|
||||
console.info("[DBX][openData:get-columns:start]", {
|
||||
traceId,
|
||||
database: node.database,
|
||||
schema: querySchema,
|
||||
table: node.label,
|
||||
elapsed: elapsed(),
|
||||
});
|
||||
const columns = await api.getColumns(node.connectionId, node.database, querySchema, node.label);
|
||||
console.info("[DBX][openData:get-columns:done]", {
|
||||
traceId,
|
||||
columnCount: columns.length,
|
||||
primaryKeys: columns.filter((column) => column.is_primary_key).map((column) => column.name),
|
||||
elapsed: elapsed(),
|
||||
});
|
||||
const pks = editablePrimaryKeys(config.db_type, columns);
|
||||
const limit = settingsStore.editorSettings.pageSize;
|
||||
const sql = await buildTableSelectSql({
|
||||
databaseType: config.db_type,
|
||||
schema: node.schema,
|
||||
tableName: node.label,
|
||||
columns: columns.map((column) => column.name),
|
||||
primaryKeys: pks,
|
||||
columns: [],
|
||||
primaryKeys: [],
|
||||
limit,
|
||||
includeRowId: usesSyntheticRowIdKey(config.db_type, pks),
|
||||
includeRowId: false,
|
||||
});
|
||||
console.info("[DBX][openData:sql-built]", {
|
||||
traceId,
|
||||
primaryKeys: pks,
|
||||
includeRowId: usesSyntheticRowIdKey(config.db_type, pks),
|
||||
primaryKeys: [],
|
||||
includeRowId: false,
|
||||
sql,
|
||||
elapsed: elapsed(),
|
||||
});
|
||||
queryStore.updateSql(tabId, sql);
|
||||
queryStore.setTableMeta(tabId, {
|
||||
schema: node.schema,
|
||||
tableName: node.label,
|
||||
columns,
|
||||
primaryKeys: pks,
|
||||
});
|
||||
|
||||
const loadTableMeta = async () => {
|
||||
try {
|
||||
console.info("[DBX][openData:get-columns:start]", {
|
||||
traceId,
|
||||
database: node.database,
|
||||
schema: querySchema,
|
||||
table: node.label,
|
||||
elapsed: elapsed(),
|
||||
});
|
||||
const columns = await api.getColumns(node.connectionId, node.database, querySchema, node.label);
|
||||
console.info("[DBX][openData:get-columns:done]", {
|
||||
traceId,
|
||||
columnCount: columns.length,
|
||||
primaryKeys: columns.filter((column) => column.is_primary_key).map((column) => column.name),
|
||||
elapsed: elapsed(),
|
||||
});
|
||||
const pks = editablePrimaryKeys(config.db_type, columns);
|
||||
queryStore.setTableMeta(tabId, {
|
||||
schema: node.schema,
|
||||
tableName: node.label,
|
||||
columns,
|
||||
primaryKeys: pks,
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn("[DBX][openData:get-columns:error]", { traceId, elapsed: elapsed(), error });
|
||||
}
|
||||
};
|
||||
|
||||
console.info("[DBX][openData:execute:start]", { traceId, tabId, elapsed: elapsed() });
|
||||
await queryStore.executeTabSql(tabId, sql);
|
||||
console.info("[DBX][openData:execute:done]", { traceId, tabId, elapsed: elapsed() });
|
||||
void loadTableMeta();
|
||||
} catch (e: any) {
|
||||
console.error("[DBX][openData:error]", { traceId, elapsed: elapsed(), error: e });
|
||||
queryStore.setErrorResult(tabId, e);
|
||||
|
||||
Reference in new issue
Block a user