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,