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
+9
View File
@@ -0,0 +1,9 @@
# Phase 28 — Front-screen charts
- The application opens on the Dashboard instead of the SQL editor.
- A Dashboard toolbar button returns to the performance dashboard from anywhere.
- Six colour-coded Swift Charts panels are embedded above server counters: queries/sec, connected threads, running threads, slow queries/sec, bytes received/sec, and bytes sent/sec.
- Charts use actual MySQL SHOW GLOBAL STATUS readings and refresh every 5 seconds while the administration screen is open.
- The first rate value requires two successful samples; no synthetic data is used.
- Retains the Phase 27 SQL-based schema editor and the existing Trends window for other administration pages.
- Build and live MySQL verification must be performed on macOS with Xcode.
+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 {
@@ -32,3 +32,4 @@
/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app
/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app
/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app
/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app
@@ -19,7 +19,7 @@
<dict>
<key>cdhash</key>
<data>
HkhN7yfGWFkk5ENSYPEwPzG7Oxw=
hwxQ/HWDtKhUDsc6szxKFot4NHU=
</data>
<key>requirement</key>
<string>identifier "WorkbenchReplacement.debug" and anchor apple generic and certificate leaf[subject.CN] = "Apple Development: Mark Gregory (FLBRJ3BYDD)" and certificate 1[field.1.2.840.113635.100.6.2.1] /* exists */</string>
+5 -5
View File
@@ -48,7 +48,7 @@
<key>uniqueIdentifier</key>
<string>4C1297D6-59C6-453E-95AE-E4B963DF2DA6</string>
</dict>
<key>F4C8C8BA-B9A7-46F3-8FF6-180215A4D5E4</key>
<key>FDF8B075-055A-4667-94AC-5A68755964AD</key>
<dict>
<key>className</key>
<string>IDECommandLineBuildLog</string>
@@ -57,7 +57,7 @@
<key>domainType</key>
<string>Xcode.IDEActivityLogDomainType.BuildLog</string>
<key>fileName</key>
<string>F4C8C8BA-B9A7-46F3-8FF6-180215A4D5E4.xcactivitylog</string>
<string>FDF8B075-055A-4667-94AC-5A68755964AD.xcactivitylog</string>
<key>hasPrimaryLog</key>
<true/>
<key>primaryObservable</key>
@@ -82,13 +82,13 @@
<key>signature</key>
<string>Building project WorkbenchReplacement with scheme WorkbenchReplacement and configuration Debug</string>
<key>timeStartedRecording</key>
<real>813918930.91679394</real>
<real>813923798.47520006</real>
<key>timeStoppedRecording</key>
<real>813918948.64702797</real>
<real>813923817.27732801</real>
<key>title</key>
<string>Building project WorkbenchReplacement with scheme WorkbenchReplacement and configuration Debug</string>
<key>uniqueIdentifier</key>
<string>F4C8C8BA-B9A7-46F3-8FF6-180215A4D5E4</string>
<string>FDF8B075-055A-4667-94AC-5A68755964AD</string>
</dict>
</dict>
</dict>
@@ -1 +1 @@
1792230778.133908: Module build session file for module cache at Path(str: "/Volumes/Developer/Applications/WorkbenchReplacement/build/ModuleCache.noindex")
1792230998.8597: Module build session file for module cache at Path(str: "/Volumes/Developer/Applications/WorkbenchReplacement/build/ModuleCache.noindex")
+1 -1
View File
@@ -3,7 +3,7 @@
<plist version="1.0">
<dict>
<key>LastAccessedDate</key>
<date>2026-10-17T09:52:58Z</date>
<date>2026-10-17T09:56:38Z</date>
<key>WorkspacePath</key>
<string>/Volumes/Developer/Applications/WorkbenchReplacement/WorkbenchReplacement.xcodeproj</string>
</dict>