feat(query): show database server messages in query results
This commit is contained in:
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>
|
||||
Reference in new issue
Block a user