Improved progress display

Previously, the progress bar updated only after completing an entire table.

For large tables such as LIBARC, this could make the application appear to have stopped responding.

Phase 10 updates the interface while rows are being exported.
This commit is contained in:
Mark Gregory
2026-10-18 11:36:34 +01:00
parent 08a73d5171
commit d96fc225cf
4 changed files with 106 additions and 37 deletions
+77 -37
View File
@@ -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,
+9
View File
@@ -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.
@@ -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.
+10
View File
@@ -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.