diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index 96257cb41c..e8a105149a 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -1918,7 +1918,7 @@ onUnmounted(() => { @editor-selection-state-change="(tabId: string, selection: { anchor: number; head: number }) => queryStore.updateEditorSelection(tabId, selection)" @format-error="toast(t('toolbar.formatSqlFailed'))" @save-sql="void openSaveSqlDialog()" - @reload="(sql?: string, searchText?: string, whereInput?: string, orderBy?: string, limit?: number, offset?: number) => onReloadData(sql, searchText, whereInput, orderBy, limit, offset)" + @reload="(sql, searchText, whereInput, orderBy, limit, offset, intent) => onReloadData(sql, searchText, whereInput, orderBy, limit, offset, intent)" @paginate="onPaginate" @sort="onSort" @execute-sql="onExecuteSql" diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index e5cc33d356..2938ddc591 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -177,6 +177,7 @@ import { useConnectionStore } from "@/stores/connectionStore"; import { useQueryStore } from "@/stores/queryStore"; import { useSettingsStore } from "@/stores/settingsStore"; import type { DataGridSortDirection, DataGridSortMode } from "@/lib/dataGrid/dataGridSort"; +import { DATA_GRID_COMPACT_TOPBAR_WIDTH, type DataGridReloadIntent } from "@/lib/dataGrid/dataGridToolbar"; import { getTableMetadataCapabilities } from "@/lib/table/tableMetadataCapabilities"; import { getTableStructureCapabilities } from "@/lib/table/tableStructureCapabilities"; import { supportsTableStructureEditing } from "@/lib/database/databaseCapabilities"; @@ -280,7 +281,6 @@ const dataGridElapsed = () => `${Math.round(performance.now() - dataGridCreatedA const isMac = isMacOS(); const shortcutMod = isMac ? "Cmd" : "Ctrl"; const saveShortcutLabel = computed(() => formatShortcut(settingsStore.editorSettings.shortcuts.saveSql)); -const DATA_GRID_COMPACT_TOPBAR_WIDTH = 900; const AUTO_REFRESH_INTERVAL_OPTIONS = [5, 10, 30, 60, 300]; function logDataGridTiming(message: string, payload?: Record) { @@ -288,7 +288,7 @@ function logDataGridTiming(message: string, payload?: Record) { } const emit = defineEmits<{ - reload: [sql?: string, searchText?: string, whereInput?: string, orderBy?: string, limit?: number, offset?: number]; + reload: [sql?: string, searchText?: string, whereInput?: string, orderBy?: string, limit?: number, offset?: number, intent?: DataGridReloadIntent]; paginate: [offset: number, limit: number, whereInput?: string, orderBy?: string]; sort: [column: string, columnIndex: number, direction: "asc" | "desc" | null, whereInput?: string, mode?: DataGridSortMode]; "update:whereInput": [value: string]; @@ -4063,6 +4063,8 @@ const saveToolbarState = computed(() => }), ); const hasSearchBarSlot = computed(() => !!slots["search-bar"]); +const hasResultToolbarLeadingSlot = computed(() => !!slots["result-toolbar-leading"]); +const hasResultToolbarActionsSlot = computed(() => !!slots["result-toolbar-actions"]); const quickEntryEnabled = computed(() => settingsStore.editorSettings.dataGridQuickEntry); const showQuickEntryDraftRow = computed(() => shouldShowQuickEntryDraftRow({ @@ -4079,6 +4081,8 @@ const showDataGridTopbar = computed( hasLocalColumnFilters.value || canShowWhereSearch.value || hasSearchBarSlot.value || + hasResultToolbarLeadingSlot.value || + hasResultToolbarActionsSlot.value || showQueryEditReadOnlyBadge.value || props.context !== "results" || (!!props.editable && hasDataGridSaveTarget.value) || @@ -4236,7 +4240,7 @@ async function onToolbarRefresh() { } preserveTransposeOnNextResult.value = showTranspose.value; isRefreshingData.value = true; - emit("reload", props.sql, searchText.value, currentWhereInput(), currentOrderBy(), pageSize.value, (currentPage.value - 1) * pageSize.value); + emit("reload", props.sql, searchText.value, currentWhereInput(), currentOrderBy(), pageSize.value, (currentPage.value - 1) * pageSize.value, "refresh"); } function stopAutoRefreshTimer() { @@ -8731,6 +8735,9 @@ const gridContextMenuItems = computed(() => {
+
+ +
+
@@ -11460,9 +11468,12 @@ const gridContextMenuItems = computed(() => { } .data-grid-topbar-action-button { + align-items: center; + justify-content: center; max-width: 9rem; min-width: 1.25rem; gap: 0; + line-height: 1; overflow: hidden; transition: max-width var(--data-grid-topbar-transition-duration) var(--data-grid-topbar-transition-easing), @@ -11484,6 +11495,8 @@ const gridContextMenuItems = computed(() => { } .data-grid-topbar-action-icon { + display: block; + align-self: center; flex-shrink: 0; transition: margin-inline-end var(--data-grid-topbar-transition-duration) var(--data-grid-topbar-transition-easing), @@ -11507,10 +11520,13 @@ const gridContextMenuItems = computed(() => { } .data-grid-topbar-action-label { - display: inline-block; + display: inline-flex; + align-items: center; + height: 1rem; max-width: 8rem; overflow: hidden; white-space: nowrap; + line-height: 1; opacity: 1; transform: translateX(0); transition: diff --git a/apps/desktop/src/components/layout/ContentArea.vue b/apps/desktop/src/components/layout/ContentArea.vue index 28d8af1d4a..bc433bd459 100644 --- a/apps/desktop/src/components/layout/ContentArea.vue +++ b/apps/desktop/src/components/layout/ContentArea.vue @@ -5,7 +5,7 @@ import { appendDebugLog, isDebugLoggingEnabled } from "@/lib/backend/debugLog"; import { canReloadUnavailableDataTab } from "@/lib/table/tableDataRefresh"; import type { CSSProperties } from "vue"; import { useI18n } from "vue-i18n"; -import { Check, Columns3, Columns3Cog, EyeOff, Loader2, Search, GitBranch, BarChart3, TableProperties, ChevronDown, ChevronUp, Inbox, RefreshCcw, Timer, Wrench, Toolbox, ListChecks, Database, Download, Upload, X, Pin, Rows3, SquareDashed, Minus, Plus } from "@lucide/vue"; +import { Check, Columns3, Columns3Cog, EyeOff, Loader2, Search, TableProperties, ChevronDown, ChevronUp, Inbox, RefreshCcw, Wrench, Toolbox, Database, Download, Upload, X, Pin, Rows3, SquareDashed, Minus, Plus } from "@lucide/vue"; import { Splitpanes, Pane } from "splitpanes"; import "splitpanes/dist/splitpanes.css"; import { Button } from "@/components/ui/button"; @@ -17,6 +17,8 @@ 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 QueryResultToolbarActions from "@/components/layout/QueryResultToolbarActions.vue"; +import QueryResultViewSwitcher from "@/components/layout/QueryResultViewSwitcher.vue"; import type { ColumnInfo } from "@/components/editor/ColumnInfoPanel.vue"; let dataGridComponentPromise: Promise | undefined; function loadDataGridComponent() { @@ -59,7 +61,7 @@ import { useConnectionStore } from "@/stores/connectionStore"; import { TABLE_FONT_SIZE_MAX, TABLE_FONT_SIZE_MIN, useSettingsStore, type DataGridSearchMode } from "@/stores/settingsStore"; import { useToast } from "@/composables/useToast"; import { canCancelQueryExecution, queryExecutionLabelKey } from "@/lib/sql/queryExecutionState"; -import { databaseDisplayNameForTab, executionSummaryItems, nextExecutionSummaryView, resultGridCacheKey, resultRunItems, resultSqlForGrid, tabularResultItems } from "@/lib/tabs/tabPresentation"; +import { databaseDisplayNameForTab, executionSummaryItems, resultGridCacheKey, resultRunItems, resultSqlForGrid, tabularResultItems } from "@/lib/tabs/tabPresentation"; import { defaultQueryResultArchiveFileName } from "@/lib/query/queryResultArchive"; import { saveQueryResultArchiveFile } from "@/lib/query/queryResultArchiveFile"; import { isTableDataEditable } from "@/lib/table/tableEditing"; @@ -72,8 +74,8 @@ import * as api from "@/lib/backend/api"; import { applyMongoGridChangesToDocument, buildMongoUpdateDocument, formatMongoShellLiteral, type MongoInputValue } from "@/lib/mongo/mongoDocumentValues"; import type { SqlExecutionOverride } from "@/lib/sql/sqlExecutionTarget"; import type { DataGridSortMode } from "@/lib/dataGrid/dataGridSort"; +import { DATA_GRID_COMPACT_TOPBAR_WIDTH, type DataGridReloadIntent } from "@/lib/dataGrid/dataGridToolbar"; import { useTabScroll } from "@/composables/useTabScroll"; -import { nextResultToolbarLayout } from "@/lib/tabs/resultToolbarLayout"; import { formatElapsedSeconds } from "@/lib/common/elapsedTime"; import type { CustomSaveHandler } from "@/composables/useDataGridEditor"; import type { QueryTab, ConnectionConfig, TableInfoTab, TreeNode, VectorCollectionMeta, ObjectBrowserViewport } from "@/types/database"; @@ -137,7 +139,7 @@ const emit = defineEmits<{ editorViewportChange: [tabId: string, viewport: { scrollTop: number; scrollLeft: number }]; editorSelectionStateChange: [tabId: string, selection: { anchor: number; head: number }]; formatError: []; - reload: [sql?: string, searchText?: string, whereInput?: string, orderBy?: string, limit?: number, offset?: number]; + reload: [sql?: string, searchText?: string, whereInput?: string, orderBy?: string, limit?: number, offset?: number, intent?: DataGridReloadIntent]; paginate: [offset: number, limit: number, whereInput?: string, orderBy?: string]; sort: [column: string, columnIndex: number, direction: "asc" | "desc" | null, whereInput?: string, mode?: DataGridSortMode]; executeSql: [sql: string]; @@ -154,7 +156,7 @@ const emit = defineEmits<{ openConnectionSettings: [connectionId: string, initialTab: "advanced"]; }>(); -const { t, locale } = useI18n(); +const { t } = useI18n(); const queryStore = useQueryStore(); const connectionStore = useConnectionStore(); const settingsStore = useSettingsStore(); @@ -186,7 +188,8 @@ const columnInfoLoading = ref(false); const columnInfoError = ref(undefined); const dataGridRef = ref(); const queryEditorRef = ref>(); -const resultToolbarRef = ref(null); +const standaloneResultToolbarRef = ref(null); +const standaloneResultToolbarWidth = ref(0); const resultTabsScrollerRef = ref(null); const columnVisibilitySearch = ref(""); const columnVisibilityOptions = computed(() => dataGridRef.value?.filteredColumnVisibilityOptions(columnVisibilitySearch.value) ?? []); @@ -313,11 +316,7 @@ const activeResultSql = computed(() => resultSqlForGrid(props.activeTab)); const resultArchiveExporting = ref(false); const canExportResultArchive = computed(() => props.activeTab.mode === "query" && (!!props.activeTab.result || !!props.activeTab.results?.length || !!props.activeTab.resultRuns?.length)); const resultAutoSave = computed(() => props.activeTab.resultAutoSave === true); -const QUERY_RESULT_AUTO_REFRESH_INTERVAL_OPTIONS = [5, 10, 30, 60, 300]; -const queryResultAutoRefreshIntervalSeconds = ref(10); -const queryResultAutoRefreshEnabled = ref(false); -let queryResultAutoRefreshTimer: ReturnType | undefined; -const queryResultAutoRefreshLabel = computed(() => (queryResultAutoRefreshEnabled.value ? t("tabs.autoRefreshEvery", { seconds: queryResultAutoRefreshIntervalSeconds.value }) : t("tabs.autoRefresh"))); +const showResultRunSelector = computed(() => resultAutoSave.value && resultRuns.value.length > 0); watch( () => visibleResultItems.value.map((item) => item.index).join(","), () => { @@ -331,80 +330,25 @@ const hasTabularResult = computed(() => { return visibleResultItems.value.length > 0; }); const canShowResultOutput = computed(() => hasTabularResult.value || props.activeTab.isExecuting); -const resultToolbarCompact = ref(false); -const resultToolbarExpandedRequiredWidth = ref(); -let resultToolbarResizeObserver: ResizeObserver | undefined; -let resultToolbarLayoutFrame = 0; +const canShowExplainOutput = computed(() => !!props.activeTab.explainPlan || !!props.activeTab.explainError || !!props.activeTab.explainTableResult || !!props.activeTab.explainTableError || props.activeTab.isExplaining === true); +const showStandaloneResultToolbar = computed(() => props.activeOutputView !== "result" || !props.activeTab.result || !hasTabularResult.value); +const standaloneResultToolbarCompact = computed(() => standaloneResultToolbarWidth.value > 0 && standaloneResultToolbarWidth.value < DATA_GRID_COMPACT_TOPBAR_WIDTH); +let standaloneResultToolbarResizeObserver: ResizeObserver | undefined; -function updateResultToolbarLayout() { - const toolbar = resultToolbarRef.value; - const tabs = resultTabsScrollerRef.value; - if (!toolbar || !tabs) return; - - const next = nextResultToolbarLayout({ - resultCount: visibleResultItems.value.length, - compact: resultToolbarCompact.value, - expandedRequiredWidth: resultToolbarExpandedRequiredWidth.value, - toolbarWidth: toolbar.clientWidth, - tabsViewportWidth: tabs.clientWidth, - tabsContentWidth: tabs.scrollWidth, - }); - const compactChanged = next.compact !== resultToolbarCompact.value; - resultToolbarCompact.value = next.compact; - resultToolbarExpandedRequiredWidth.value = next.expandedRequiredWidth; - if (compactChanged) { - nextTick(() => { - updateResultTabsScrollbar(); - scheduleResultToolbarLayout(); +function observeStandaloneResultToolbar() { + standaloneResultToolbarResizeObserver?.disconnect(); + standaloneResultToolbarResizeObserver = undefined; + const toolbar = standaloneResultToolbarRef.value; + standaloneResultToolbarWidth.value = toolbar?.clientWidth ?? 0; + if (toolbar && typeof ResizeObserver !== "undefined") { + standaloneResultToolbarResizeObserver = new ResizeObserver(() => { + standaloneResultToolbarWidth.value = toolbar.clientWidth; }); + standaloneResultToolbarResizeObserver.observe(toolbar); } } -function scheduleResultToolbarLayout() { - if (resultToolbarLayoutFrame) return; - resultToolbarLayoutFrame = window.requestAnimationFrame(() => { - resultToolbarLayoutFrame = 0; - updateResultToolbarLayout(); - }); -} - -function resetResultToolbarLayout() { - resultToolbarCompact.value = false; - resultToolbarExpandedRequiredWidth.value = undefined; - nextTick(() => { - updateResultTabsScrollbar(); - scheduleResultToolbarLayout(); - }); -} - -function observeResultToolbarLayout() { - resultToolbarResizeObserver?.disconnect(); - resultToolbarResizeObserver = undefined; - const toolbar = resultToolbarRef.value; - const tabs = resultTabsScrollerRef.value; - if (typeof ResizeObserver !== "undefined" && toolbar && tabs) { - resultToolbarResizeObserver = new ResizeObserver(scheduleResultToolbarLayout); - resultToolbarResizeObserver.observe(toolbar); - resultToolbarResizeObserver.observe(tabs); - } - scheduleResultToolbarLayout(); -} - -const resultToolbarLayoutSignature = computed(() => - JSON.stringify({ - locale: locale.value, - results: visibleResultItems.value.map((item) => [item.index, item.label, item.n]), - outputView: props.activeOutputView, - canExportResultArchive: canExportResultArchive.value, - hasResult: !!props.activeTab.result, - hasTabularResult: hasTabularResult.value, - autoRefreshEnabled: queryResultAutoRefreshEnabled.value, - autoRefreshInterval: queryResultAutoRefreshIntervalSeconds.value, - }), -); - -watch([resultToolbarRef, resultTabsScrollerRef], observeResultToolbarLayout, { flush: "post" }); -watch(resultToolbarLayoutSignature, resetResultToolbarLayout, { flush: "post" }); +watch(standaloneResultToolbarRef, observeStandaloneResultToolbar, { flush: "post" }); type MongoQueryGridChanges = { dirtyRows: Map>; deletedRows: Set; @@ -482,7 +426,6 @@ const resultsPaneOpen = ref(false); const resultsPaneSize = ref(Number(safeLocalStorageGet("dbx-results-pane-size")) || DEFAULT_QUERY_RESULTS_PANE_SIZE); const editorPaneSize = computed(() => (resultsPaneOpen.value ? 100 - resultsPaneSize.value : 100)); const queryRunningElapsed = ref(0); -const canAutoRefreshQueryResult = computed(() => props.activeTab.mode === "query" && props.activeOutputView === "result" && resultsPaneOpen.value && hasTabularResult.value && !props.activeTab.isExecuting); function onResultsResized(payload: { panes: { size: number }[] }) { const resultsPane = payload.panes[1]; @@ -524,31 +467,10 @@ watch(() => [props.activeTab.id, props.activeTab.isExecuting, props.activeTab.qu onUnmounted(() => { stopQueryRunningElapsedTimer(); - stopQueryResultAutoRefreshTimer(); - resultToolbarResizeObserver?.disconnect(); - if (resultToolbarLayoutFrame) window.cancelAnimationFrame(resultToolbarLayoutFrame); + standaloneResultToolbarResizeObserver?.disconnect(); window.removeEventListener("dbx-refresh-active-kv-browser", onRefreshActiveKvBrowser); }); -watch(() => props.activeTab.id, stopQueryResultAutoRefresh); - -watch( - () => [props.activeOutputView, resultsPaneOpen.value] as const, - ([outputView, paneOpen]) => { - if (outputView !== "result" || !paneOpen) stopQueryResultAutoRefresh(); - }, -); - -watch(canAutoRefreshQueryResult, (canRefresh) => { - if (!queryResultAutoRefreshEnabled.value) return; - if (canRefresh) restartQueryResultAutoRefreshTimer(); - else stopQueryResultAutoRefreshTimer(); -}); - -watch(activeQueryError, (message) => { - if (message && queryResultAutoRefreshEnabled.value) stopQueryResultAutoRefresh(); -}); - watch( hasQueryOutput, (hasOutput) => { @@ -734,37 +656,6 @@ function focusSearch(): boolean { return dataGridRef.value?.focusSearch() ?? false; } -function stopQueryResultAutoRefreshTimer() { - clearInterval(queryResultAutoRefreshTimer); - queryResultAutoRefreshTimer = undefined; -} - -function runQueryResultAutoRefreshTick() { - if (!queryResultAutoRefreshEnabled.value || !canAutoRefreshQueryResult.value) return; - refreshData(); -} - -function restartQueryResultAutoRefreshTimer() { - stopQueryResultAutoRefreshTimer(); - if (!queryResultAutoRefreshEnabled.value || !canAutoRefreshQueryResult.value) return; - queryResultAutoRefreshTimer = setInterval(runQueryResultAutoRefreshTick, queryResultAutoRefreshIntervalSeconds.value * 1000); -} - -function setQueryResultAutoRefreshInterval(seconds: number) { - queryResultAutoRefreshIntervalSeconds.value = seconds; - if (queryResultAutoRefreshEnabled.value) restartQueryResultAutoRefreshTimer(); -} - -function toggleQueryResultAutoRefresh() { - queryResultAutoRefreshEnabled.value = !queryResultAutoRefreshEnabled.value; - restartQueryResultAutoRefreshTimer(); -} - -function stopQueryResultAutoRefresh() { - queryResultAutoRefreshEnabled.value = false; - stopQueryResultAutoRefreshTimer(); -} - function refreshData(): boolean { if (props.activeTab.mode === "etcd") return etcdKeyBrowserRef.value?.refresh?.() ?? false; if (props.activeTab.mode === "zookeeper") return zookeeperKeyBrowserRef.value?.refresh?.() ?? false; @@ -802,10 +693,6 @@ async function exportResultArchive() { } } -function toggleExecutionSummary() { - emit("update:activeOutputView", nextExecutionSummaryView(props.activeOutputView, canShowResultOutput.value)); -} - async function removeResultRun(runId: string) { const removedActiveRun = props.activeTab.activeResultRunId === runId; const removed = await queryStore.removeResultRun(props.activeTab.id, runId); @@ -891,18 +778,13 @@ defineExpose({ focusSearch, refreshData, handleModRTarget, requestQueryEditorExe
-
-
- -
+
-