diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index 2979afa64..7dd12736b 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -239,7 +239,7 @@ const selectedSql = ref(""); const cursorPos = ref(0); const formatSqlRequest = ref<{ id: number; tabId: string } | null>(null); const compressSqlRequest = ref<{ id: number; tabId: string } | null>(null); -const activeOutputView = ref<"result" | "summary" | "explain" | "chart">("result"); +const activeOutputView = ref<"result" | "summary" | "explain" | "chart" | "messages">("result"); const newQueryContextSource = ref<"tab" | "sidebar">("tab"); const queryEditorDdlTarget = ref<{ connectionId: string; database: string; catalog?: string; schema?: string; tableName: string; objectType?: ObjectSourceKind } | null>(null); const queryEditorObjectSourceTarget = ref<{ diff --git a/apps/desktop/src/components/layout/ContentArea.vue b/apps/desktop/src/components/layout/ContentArea.vue index 0e71a96d5..ccc082bdb 100644 --- a/apps/desktop/src/components/layout/ContentArea.vue +++ b/apps/desktop/src/components/layout/ContentArea.vue @@ -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 + + @@ -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)" > - + +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 = { + 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", +}; + + + + + + {{ t("queryMessages.empty") }} + + + + + {{ message.severity }} + + + {{ message.message }} + {{ message.detail }} + {{ message.hint }} + {{ t("queryMessages.code", { code: message.code }) }} + + + + + diff --git a/apps/desktop/src/components/layout/QueryResultToolbarActions.vue b/apps/desktop/src/components/layout/QueryResultToolbarActions.vue index caa264b4b..cc12b58cf 100644 --- a/apps/desktop/src/components/layout/QueryResultToolbarActions.vue +++ b/apps/desktop/src/components/layout/QueryResultToolbarActions.vue @@ -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<{ diff --git a/apps/desktop/src/components/layout/QueryResultViewSwitcher.vue b/apps/desktop/src/components/layout/QueryResultViewSwitcher.vue index e90347c2a..3ace1eab6 100644 --- a/apps/desktop/src/components/layout/QueryResultViewSwitcher.vue +++ b/apps/desktop/src/components/layout/QueryResultViewSwitcher.vue @@ -1,10 +1,11 @@