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-17 10:59:05 +01:00
parent c511256f32
commit c3fb3e6fad
22 changed files with 406 additions and 230 deletions
+98 -4
View File
@@ -52,7 +52,7 @@ struct MainView: View {
@State private var outputExpanded = true
@State private var navigatorMode = 1
@State private var administrationPage: WorkbenchAdminPage? = nil
@State private var administrationPage: WorkbenchAdminPage? = .dashboard
var body: some View {
@@ -142,6 +142,11 @@ struct MainView: View {
.fill(WB.border)
.frame(width: 1, height: 30)
toolbarButton("Dashboard", icon: "chart.xyaxis.line") {
navigatorMode = 0
administrationPage = .dashboard
}
toolbarButton(
"New Connection",
icon: "plus.rectangle.on.rectangle"
@@ -2215,7 +2220,7 @@ struct WorkbenchAdministrationView: View {
@State private var errorMessage: String?
@State private var lastUpdated: Date?
@State private var searchText = ""
@State private var autoRefresh = false
@State private var autoRefresh = true
@State private var selectedAccount: String? = nil
@State private var grants: [String] = []
@State private var grantsError: String? = nil
@@ -2307,7 +2312,7 @@ struct WorkbenchAdministrationView: View {
.padding(.vertical, 8)
}
if supportsTrends && !diagnosticSamples.isEmpty {
if supportsTrends && !diagnosticSamples.isEmpty && page != .dashboard && page != .serverStatus {
HStack(spacing: 12) {
Label("\(diagnosticSamples.count) samples", systemImage: "chart.xyaxis.line")
.foregroundStyle(.secondary)
@@ -2539,7 +2544,11 @@ struct WorkbenchAdministrationView: View {
private var resultContent: some View {
if page == .dashboard || page == .serverStatus {
ScrollView {
LazyVGrid(
VStack(alignment: .leading, spacing: 18) {
WorkbenchInlinePerformanceDashboard(samples: diagnosticSamples)
Text("Server counters")
.font(.headline)
LazyVGrid(
columns: [
GridItem(.adaptive(minimum: 180), spacing: 12)
],
@@ -2586,6 +2595,7 @@ struct WorkbenchAdministrationView: View {
}
}
}
}
.padding(18)
}
} else {
@@ -4103,6 +4113,90 @@ private struct WorkbenchAccountManagerView: View {
}
// MARK: - Phase 28: Front-screen performance charts
private struct WorkbenchInlinePerformanceDashboard: View {
let samples: [DiagnosticSample]
private struct Reading: Identifiable {
let id: Int
let time: Date
let value: Double
}
private func counter(_ sample: DiagnosticSample, _ name: String) -> Double? {
guard let row = sample.rows.first(where: {
$0.count > 1 && $0[0].caseInsensitiveCompare(name) == .orderedSame
}) else { return nil }
return Double(row[1])
}
private func points(_ name: String, rate: Bool) -> [Reading] {
samples.indices.compactMap { i in
guard let current = counter(samples[i], name) else { return nil }
if !rate { return Reading(id: i, time: samples[i].at, value: current) }
guard i > 0, let previous = counter(samples[i - 1], name) else { return nil }
let interval = samples[i].at.timeIntervalSince(samples[i - 1].at)
guard interval > 0, current >= previous else { return nil }
return Reading(id: i, time: samples[i].at, value: (current - previous) / interval)
}
}
private func panel(_ title: String, metric: String, rate: Bool, tint: Color) -> some View {
let readings = points(metric, rate: rate)
return VStack(alignment: .leading, spacing: 10) {
HStack {
Text(title).font(.headline)
Spacer()
Circle().fill(tint).frame(width: 9, height: 9)
}
if let last = readings.last {
Text(last.value.formatted(.number.precision(.fractionLength(1))) + (rate ? " /sec" : ""))
.font(.system(size: 24, weight: .semibold, design: .rounded))
.foregroundStyle(tint)
} else {
Text("Collecting samples…")
.font(.caption).foregroundStyle(.secondary)
}
Chart(readings) { reading in
AreaMark(x: .value("Time", reading.time), y: .value("Value", reading.value))
.foregroundStyle(tint.opacity(0.14))
LineMark(x: .value("Time", reading.time), y: .value("Value", reading.value))
.foregroundStyle(tint)
.lineStyle(StrokeStyle(lineWidth: 2.5))
}
.chartYAxis { AxisMarks(position: .leading) }
.frame(height: 130)
}
.padding(14)
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color(nsColor: .controlBackgroundColor))
.clipShape(RoundedRectangle(cornerRadius: 12))
}
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Label("Live server performance", systemImage: "waveform.path.ecg")
.font(.title2.bold())
Spacer()
Text("\(samples.count) samples · refreshes every 5 seconds")
.font(.caption).foregroundStyle(.secondary)
}
LazyVGrid(columns: [GridItem(.adaptive(minimum: 270), spacing: 12)], spacing: 12) {
panel("Queries per second", metric: "Queries", rate: true, tint: .blue)
panel("Active connections", metric: "Threads_connected", rate: false, tint: .teal)
panel("Running threads", metric: "Threads_running", rate: false, tint: .orange)
panel("Slow queries per second", metric: "Slow_queries", rate: true, tint: .red)
panel("Bytes received per second", metric: "Bytes_received", rate: true, tint: .purple)
panel("Bytes sent per second", metric: "Bytes_sent", rate: true, tint: .green)
}
Text("Rates are calculated from consecutive MySQL status snapshots. Counters reset after server restarts; graphs show up to 60 in-session samples.")
.font(.caption).foregroundStyle(.secondary)
}
}
}
// MARK: - Phase 25/26: Live performance samples and counter rates
private struct DiagnosticSample: Identifiable {