diff --git a/WorkbenchReplacement/MainView.swift b/WorkbenchReplacement/MainView.swift index 5765ed8..5371109 100644 --- a/WorkbenchReplacement/MainView.swift +++ b/WorkbenchReplacement/MainView.swift @@ -52,6 +52,7 @@ struct MainView: View { @State private var outputExpanded = true @State private var navigatorMode = 1 + @State private var sqlFocusRequest = 0 @State private var administrationPage: WorkbenchAdminPage? = .dashboard @@ -126,6 +127,21 @@ struct MainView: View { } } + // MARK: - Query execution navigation + + /// A query launched from the global toolbar must reveal its editor and + /// results, even when an Administration page currently owns the workspace. + private func executeSQLInEditor() { + navigatorMode = 1 + administrationPage = nil + model.runSQL() + // The SQLTextView is created only after the workspace changes. Request + // focus on the next main-loop turn, after SwiftUI mounts that view. + DispatchQueue.main.async { + sqlFocusRequest += 1 + } + } + // MARK: - Main Toolbar private var mainToolbar: some View { @@ -175,7 +191,7 @@ struct MainView: View { "Execute", icon: "bolt.fill" ) { - model.runSQL() + executeSQLInEditor() } .keyboardShortcut(.return, modifiers: [.command]) @@ -236,6 +252,12 @@ struct MainView: View { .pickerStyle(.segmented) .labelsHidden() .padding(8) + .onChange(of: navigatorMode) { _, newMode in + if newMode == 1 { + administrationPage = nil + sqlFocusRequest += 1 + } + } if navigatorMode == 0 { administrationNavigator @@ -800,7 +822,7 @@ struct MainView: View { .fill(WB.border) .frame(width: 1) - SQLTextView(text: $model.sql, selectedSQL: $selectedSQL, completions: Array(Set(model.schemaTables.values.flatMap { $0 } + model.schemaViews.values.flatMap { $0 } + (model.schemaColumns[model.selectedDatabase] ?? []))).sorted()) + SQLTextView(text: $model.sql, selectedSQL: $selectedSQL, focusRequest: sqlFocusRequest, completions: Array(Set(model.schemaTables.values.flatMap { $0 } + model.schemaViews.values.flatMap { $0 } + (model.schemaColumns[model.selectedDatabase] ?? []))).sorted()) } .background(Color(nsColor: .textBackgroundColor)) } @@ -1198,6 +1220,7 @@ private struct SavedSQLQueriesSheet: View { struct SQLTextView: NSViewRepresentable { @Binding var text: String @Binding var selectedSQL: String + var focusRequest: Int = 0 var completions: [String] = [] func makeCoordinator() -> Coordinator { @@ -1235,6 +1258,11 @@ struct SQLTextView: NSViewRepresentable { editor.string = text context.coordinator.highlight(editor) + // The editor may be mounted after the Schemas navigation transition. + DispatchQueue.main.async { [weak editor] in + guard let editor, let window = editor.window else { return } + window.makeFirstResponder(editor) + } return scroll } @@ -1246,6 +1274,14 @@ struct SQLTextView: NSViewRepresentable { guard let editor = scroll.documentView as? NSTextView else { return } + context.coordinator.parent = self + if context.coordinator.lastFocusRequest != focusRequest { + context.coordinator.lastFocusRequest = focusRequest + DispatchQueue.main.async { [weak editor] in + guard let editor, let window = editor.window else { return } + window.makeFirstResponder(editor) + } + } if editor.string != text { let selection = editor.selectedRange() @@ -1266,9 +1302,11 @@ struct SQLTextView: NSViewRepresentable { final class Coordinator: NSObject, NSTextViewDelegate { var parent: SQLTextView + var lastFocusRequest: Int init(_ parent: SQLTextView) { self.parent = parent + self.lastFocusRequest = parent.focusRequest } private let completionWords = [ @@ -2376,6 +2414,15 @@ struct WorkbenchAdministrationView: View { await refresh() } .task(id: "\(page.id)-\(autoRefresh)-\(profile?.id.uuidString ?? "")") { + // Gather a second snapshot promptly on first display so rate graphs + // draw a line without waiting for the full five-second interval. + if autoRefresh && supportsTrends { + try? await Task.sleep(for: .seconds(1.5)) + if !Task.isCancelled && autoRefresh { + if loading { try? await Task.sleep(for: .seconds(1)) } + if !Task.isCancelled && autoRefresh { await refresh() } + } + } while autoRefresh && !Task.isCancelled { try? await Task.sleep(for: .seconds(5)) if Task.isCancelled || !autoRefresh { break } @@ -2553,52 +2600,45 @@ struct WorkbenchAdministrationView: View { private var resultContent: some View { switch page { case .serverStatus, .dashboard: - // Phase 49: preserve the verified Phase 48 line charts, while - // restoring the server overview in its own scrollable lower pane. - GeometryReader { available in - let chartHeight = min(155.0, max(85.0, (available.size.height - 270.0) / 3.0)) - VStack(alignment: .leading, spacing: 0) { + // Phase 51: one outer scroll container owns the entire dashboard. + // The former fixed-height top chart pane consumed the available + // window height and left the overview pane impossible to reach. + ScrollView(.vertical) { + VStack(alignment: .leading, spacing: 18) { WorkbenchInlinePerformanceDashboard( samples: diagnosticSamples, - chartHeight: chartHeight + chartHeight: 155 ) - .padding(.horizontal, 18) - .padding(.top, 12) - .padding(.bottom, 8) - .fixedSize(horizontal: false, vertical: true) - .layoutPriority(1) - .accessibilityIdentifier("live-mysql-line-graphs-phase49") + .accessibilityIdentifier("live-mysql-line-graphs-phase51") Divider() - ScrollView(.vertical) { - VStack(alignment: .leading, spacing: 14) { - Label("Server overview · Live graphs", systemImage: "chart.xyaxis.line") - .font(.title3.bold()) - WorkbenchServerOverview(rows: filteredRows, samples: diagnosticSamples) - if page == .serverStatus { - Divider() - HStack { - Text("Server status metrics").font(.headline) - Spacer() - Text("\(filteredRows.count) metrics") - .font(.caption).foregroundStyle(.secondary) - } - WorkbenchColouredCounterTable( - columns: ["Metric", "Value"], rows: filteredRows - ) - .frame(height: 300) - } + Label("Server overview · Live graphs", systemImage: "chart.xyaxis.line") + .font(.title3.bold()) + + WorkbenchServerOverview(rows: filteredRows, samples: diagnosticSamples) + + if page == .serverStatus { + Divider() + HStack { + Text("Server status metrics").font(.headline) + Spacer() + Text("\(filteredRows.count) metrics") + .font(.caption).foregroundStyle(.secondary) } - .padding(18) - .frame(maxWidth: .infinity, alignment: .leading) + // The grid owns its own bounded viewport; the outer + // page can still scroll to every section below charts. + WorkbenchColouredCounterTable( + columns: ["Metric", "Value"], rows: filteredRows + ) + .frame(height: 320) } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .accessibilityIdentifier("scrollable-server-overview-phase49") } - .frame(width: available.size.width, height: available.size.height, alignment: .topLeading) + .padding(18) + .frame(maxWidth: .infinity, alignment: .leading) } .frame(maxWidth: .infinity, maxHeight: .infinity) + .accessibilityIdentifier("scrollable-dashboard-phase51") case .serverCounters, .connectionStatistics, .innodbMetrics: WorkbenchLiveMonitorScreen( page: page, diff --git a/docs/PHASE51-SCROLLING.md b/docs/PHASE51-SCROLLING.md new file mode 100644 index 0000000..d7d74a3 --- /dev/null +++ b/docs/PHASE51-SCROLLING.md @@ -0,0 +1,9 @@ +# Phase 51 — Full-page scrolling + +Baseline: Phase 50, including all 14 working live line graphs. + +The previous Dashboard / Server Status implementation put the six performance charts outside a second scrolling pane and constrained the complete layout to the window height. On typical window sizes, the six charts consumed nearly all vertical space, leaving the lower overview graphs inaccessible. + +Both pages now use a single vertical ScrollView around the six performance graphs, eight overview graphs, and (on Server Status) the status metrics grid. The existing chart components, series, styling, and five-second sampling are unchanged. Scroll down using a trackpad, mouse wheel or scroll bar to reach the overview graphs and metrics. + +This archive has not been compiled or visually tested on macOS. diff --git a/docs/PHASE52-EDITOR-FOCUS-INITIAL-GRAPHS.md b/docs/PHASE52-EDITOR-FOCUS-INITIAL-GRAPHS.md new file mode 100644 index 0000000..aa1a122 --- /dev/null +++ b/docs/PHASE52-EDITOR-FOCUS-INITIAL-GRAPHS.md @@ -0,0 +1,10 @@ +# Phase 52 — Schema editor focus and first-display graphs + +Based on the approved Phase 50 graphs and Phase 51 scrolling layout. + +- Selecting **Schemas** switches the workspace to the SQL editor and requests keyboard focus for the native NSTextView. +- The native editor receives focus on its initial mount and on subsequent navigation to Schemas; the existing editor text, selection and syntax highlighting remain intact. +- Monitoring views perform their first refresh on appearance and, when auto-refresh is enabled, attempt a follow-up snapshot after about 1.5 seconds. Rate graphs require two distinct snapshots; gauge graphs can show a first data point immediately. Normal five-second refresh continues thereafter. +- The chart designs, all eight overview graphs, six performance graphs, SQL queries and scrolling are otherwise unchanged. + +**Verification:** Swift parser checked on Linux. macOS compilation and GUI interaction testing are still required. diff --git a/docs/PHASE53-EXECUTE-REVEALS-EDITOR.md b/docs/PHASE53-EXECUTE-REVEALS-EDITOR.md new file mode 100644 index 0000000..f46cdb5 --- /dev/null +++ b/docs/PHASE53-EXECUTE-REVEALS-EDITOR.md @@ -0,0 +1,10 @@ +# Phase 53 — Execute reveals SQL editor + +Based on Phase 52; preserves the Phase 50 graph design and Phase 51 scrolling. + +- Global toolbar **Execute** and its Command-Return shortcut now navigate from Dashboard or another Administration screen to Schemas / SQL Editor before running the query. +- The query still runs through `DatabaseModel.runSQL()`; its existing results, error handling, and query history are unchanged. +- Focus is requested on the next main-loop turn after the SQL workspace is mounted. +- The in-editor Execute and Run Selection buttons retain their existing behaviour. + +Verification: Swift syntax parsing and ZIP integrity on Linux. macOS build and UI testing not available here.