feat(query): show database server messages in query results

This commit is contained in:
Eddy Lei authored and GitHub committed 2026-08-07 15:18:32 +08:00
1 parent 4c7ddd266b
commit a5e46fd659
45 files changed
+1401 -51

No files matched your search

@@ -3,6 +3,7 @@ import { computed, ref, defineAsyncComponent, watch, nextTick, onMounted, onUnmo
import { safeLocalStorageGet, safeLocalStorageSet } from "@/lib/backend/safeStorage";
import { appendDebugLog, isDebugLoggingEnabled } from "@/lib/backend/debugLog";
import { canReloadUnavailableDataTab } from "@/lib/table/tableDataRefresh";
import { defaultViewForResult } from "@/lib/query/queryResultDefaultView";
import { isQueryExecutionErrorResult } from "@/lib/query/queryResultError";
import type { CSSProperties } from "vue";
import { useI18n } from "vue-i18n";
@@ -18,6 +19,7 @@ import QueryEditor from "@/components/editor/QueryEditor.vue";
import ColumnInfoPanel from "@/components/editor/ColumnInfoPanel.vue";
import QueryLoadingState from "@/components/common/QueryLoadingState.vue";
import QueryErrorActions from "@/components/common/QueryErrorActions.vue";
import QueryMessagesView from "@/components/layout/QueryMessagesView.vue";
import QueryResultToolbarActions from "@/components/layout/QueryResultToolbarActions.vue";
import QueryResultViewSwitcher from "@/components/layout/QueryResultViewSwitcher.vue";
import DataGridCopyFormatControl from "@/components/grid/DataGridCopyFormatControl.vue";
@@ -138,7 +140,7 @@ const props = defineProps<{
activeTab: QueryTab;
activeConnection?: ConnectionConfig;
executableSql: string;
activeOutputView: "result" | "summary" | "explain" | "chart";
activeOutputView: "result" | "summary" | "explain" | "chart" | "messages";
formatSqlRequest: { id: number; tabId: string } | null;
compressSqlRequest: { id: number; tabId: string } | null;
selectedSql: string;
@@ -147,7 +149,7 @@ const props = defineProps<{
}>();
const emit = defineEmits<{
"update:activeOutputView": [value: "result" | "summary" | "explain" | "chart"];
"update:activeOutputView": [value: "result" | "summary" | "explain" | "chart" | "messages"];
fixWithAi: [errorMessage: string];
sendSelectionToAi: [sql: string];
execute: [sqlOverride?: SqlExecutionOverride];
@@ -427,6 +429,8 @@ const hasTabularResult = computed(() => {
});
const canShowResultOutput = computed(() => hasTabularResult.value || props.activeTab.isExecuting);
const canShowExplainOutput = computed(() => !!props.activeTab.explainPlan || !!props.activeTab.explainError || !!props.activeTab.explainTableResult || !!props.activeTab.explainTableError || props.activeTab.isExplaining === true);
const resultMessageCount = computed(() => props.activeTab.result?.messages?.length ?? 0);
const canShowMessagesOutput = computed(() => resultMessageCount.value > 0);
const showStandaloneResultToolbar = computed(() => activeElasticsearchJsonResponse.value || props.activeOutputView !== "result" || !props.activeTab.result || !hasTabularResult.value);
const standaloneResultToolbarCompact = computed(() => isDataGridToolbarCompact(standaloneResultToolbarWidth.value, standaloneResultToolbarViewportWidth.value));
let standaloneResultToolbarResizeObserver: ResizeObserver | undefined;
@@ -599,7 +603,8 @@ watch(
() => {
if (props.activeTab.isExecuting) return;
if (hasExecutionSummary.value && !hasTabularResult.value && props.activeOutputView === "result") {
emit("update:activeOutputView", "summary");
const result = props.activeTab.result;
emit("update:activeOutputView", result ? defaultViewForResult(result) : "summary");
}
},
{ immediate: true },
@@ -1319,6 +1324,8 @@ defineExpose({ focusSearch, refreshData, refreshQueryEditorCompletionCache, hand
:can-show-result="canShowResultOutput"
:can-show-summary="hasExecutionSummary"
:can-show-chart="hasNumericData && !activeElasticsearchJsonResponse"
:can-show-messages="canShowMessagesOutput"
:message-count="resultMessageCount"
:compact="standaloneResultToolbarCompact"
@select-view="emit('update:activeOutputView', $event)"
/>
@@ -1414,6 +1421,8 @@ defineExpose({ focusSearch, refreshData, refreshQueryEditorCompletionCache, hand
</div>
</div>
<QueryMessagesView v-else-if="activeOutputView === 'messages'" class="flex-1 min-h-0" :messages="activeTab.result?.messages ?? []" />
<template v-else>
<ElasticsearchJsonResponsePanel v-if="activeElasticsearchJsonResponse" ref="elasticsearchJsonResponsePanelRef" class="flex-1 min-h-0" :status="activeElasticsearchJsonResponse.status" :body="activeElasticsearchJsonResponse.body" />
<ElasticsearchJsonResponsePanel v-else-if="showElasticsearchRawJson && activeElasticsearchRawBody" ref="elasticsearchJsonResponsePanelRef" class="flex-1 min-h-0" :status="200" :body="activeElasticsearchRawBody" can-show-table @show-table="showElasticsearchRawJson = false" />
@@ -1467,7 +1476,16 @@ defineExpose({ focusSearch, refreshData, refreshQueryEditorCompletionCache, hand
@sort="(column: string, columnIndex: number, direction: 'asc' | 'desc' | null, whereInput?: string, mode?: DataGridSortMode) => emit('sort', column, columnIndex, direction, whereInput, mode)"
>
<template #result-toolbar-leading="{ compact }">
<QueryResultViewSwitcher :active-view="activeOutputView" :can-show-result="canShowResultOutput" :can-show-summary="hasExecutionSummary" :can-show-chart="hasNumericData && !activeElasticsearchJsonResponse" :compact="compact" @select-view="emit('update:activeOutputView', $event)" />
<QueryResultViewSwitcher
:active-view="activeOutputView"
:can-show-result="canShowResultOutput"
:can-show-summary="hasExecutionSummary"
:can-show-chart="hasNumericData && !activeElasticsearchJsonResponse"
:can-show-messages="canShowMessagesOutput"
:message-count="resultMessageCount"
:compact="compact"
@select-view="emit('update:activeOutputView', $event)"
/>
<template v-if="activeElasticsearchRawBody">
<div class="mx-1 h-4 w-px bg-border" />
<button
@@ -0,0 +1,45 @@
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import { Badge } from "@/components/ui/badge";
import type { QueryMessage } from "@/types/database";
defineProps<{ messages: QueryMessage[] }>();
const { t } = useI18n();
type SeverityTone = "muted" | "warning" | "error";
function severityTone(severity: string): SeverityTone {
const normalized = severity.toLowerCase();
if (normalized === "error" || normalized === "fatal" || normalized === "panic") return "error";
if (normalized.includes("warn")) return "warning";
return "muted";
}
const severityBadgeClasses: Record<SeverityTone, string> = {
muted: "border-border bg-muted/40 text-muted-foreground",
warning: "border-amber-500/35 bg-amber-500/10 text-amber-700 dark:text-amber-300",
error: "border-destructive/40 bg-destructive/10 text-destructive",
};
</script>
<template>
<div class="h-full overflow-auto bg-background">
<div v-if="messages.length === 0" class="flex h-full items-center justify-center text-sm text-muted-foreground">
{{ t("queryMessages.empty") }}
</div>
<div v-else class="overflow-hidden">
<div v-for="(message, index) in messages" :key="index" class="flex items-start gap-2 border-b px-3 py-2 text-xs last:border-b-0">
<Badge variant="secondary" class="mt-px shrink-0 font-mono text-[10px] uppercase" :class="severityBadgeClasses[severityTone(message.severity)]">
{{ message.severity }}
</Badge>
<div class="min-w-0 flex-1">
<div class="font-mono text-[11px] whitespace-pre-wrap break-words text-foreground">{{ message.message }}</div>
<div v-if="message.detail" class="mt-0.5 font-mono text-[11px] whitespace-pre-wrap break-words text-muted-foreground">{{ message.detail }}</div>
<div v-if="message.hint" class="mt-0.5 font-mono text-[11px] whitespace-pre-wrap break-words text-muted-foreground">{{ message.hint }}</div>
<div v-if="message.code" class="mt-0.5 font-mono text-[10px] text-muted-foreground">{{ t("queryMessages.code", { code: message.code }) }}</div>
</div>
</div>
</div>
</div>
</template>
@@ -4,7 +4,7 @@ import { useI18n } from "vue-i18n";
import { Button } from "@/components/ui/button";
import LightTooltip from "@/components/ui/LightTooltip.vue";
type OutputView = "result" | "summary" | "explain" | "chart";
type OutputView = "result" | "summary" | "explain" | "chart" | "messages";
withDefaults(
defineProps<{
@@ -1,10 +1,11 @@
<script setup lang="ts">
import { BarChart3, ListChecks } from "@lucide/vue";
import { computed } from "vue";
import { BarChart3, ListChecks, MessageSquareText } from "@lucide/vue";
import { useI18n } from "vue-i18n";
import { Button } from "@/components/ui/button";
import LightTooltip from "@/components/ui/LightTooltip.vue";
type OutputView = "result" | "summary" | "explain" | "chart";
type OutputView = "result" | "summary" | "explain" | "chart" | "messages";
type PrimaryResultView = Exclude<OutputView, "explain">;
const props = withDefaults(
@@ -13,9 +14,11 @@ const props = withDefaults(
canShowResult: boolean;
canShowSummary: boolean;
canShowChart: boolean;
canShowMessages: boolean;
messageCount?: number;
compact?: boolean;
}>(),
{ compact: false },
{ compact: false, messageCount: 0 },
);
const emit = defineEmits<{
@@ -24,6 +27,8 @@ const emit = defineEmits<{
const { t } = useI18n();
const messagesTooltip = computed(() => (props.messageCount > 0 ? `${t("tabs.messages")} (${props.messageCount})` : t("tabs.messages")));
function selectView(view: PrimaryResultView) {
if (props.activeView === view) return;
emit("selectView", view);
@@ -69,5 +74,23 @@ function selectView(view: PrimaryResultView) {
<span v-if="!compact" class="inline-flex h-4 items-center leading-none">{{ t("chart.title") }}</span>
</Button>
</LightTooltip>
<LightTooltip :text="messagesTooltip" :disabled="!compact" side="bottom" :delay="0" :close-delay="0" nowrap>
<Button
size="sm"
:variant="activeView === 'messages' ? 'secondary' : 'ghost'"
class="h-5 shrink-0 text-xs leading-none"
:class="compact ? 'w-6 gap-0 px-0' : 'gap-1 px-2'"
:title="messagesTooltip"
:aria-label="messagesTooltip"
:aria-pressed="activeView === 'messages'"
:disabled="!canShowMessages"
@click="selectView('messages')"
>
<MessageSquareText class="block h-3.5 w-3.5 self-center" />
<span v-if="!compact" class="inline-flex h-4 items-center leading-none">{{ t("tabs.messages") }}</span>
<span v-if="!compact && messageCount > 0" class="inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-muted px-1 text-[10px] leading-none tabular-nums text-muted-foreground">{{ messageCount }}</span>
</Button>
</LightTooltip>
</div>
</template>