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 11:31:54 +01:00
parent 5cf34634d2
commit e1a9b4037e
8 changed files with 127 additions and 30 deletions
+7
View File
@@ -1,4 +1,5 @@
import SwiftUI
import AppKit
@main
struct WorkbenchReplacementApp: App {
@@ -7,6 +8,12 @@ struct WorkbenchReplacementApp: App {
var body: some Scene {
WindowGroup("Workbench Replacement") {
WorkbenchLaunchView()
.onAppear {
if let iconURL = Bundle.main.url(forResource: "WorkbenchReplacement", withExtension: "icns"),
let icon = NSImage(contentsOf: iconURL) {
NSApplication.shared.applicationIconImage = icon
}
}
.environmentObject(model)
.frame(minWidth: 800, minHeight: 520)
}
+83 -30
View File
@@ -130,9 +130,10 @@ struct MainView: View {
private var mainToolbar: some View {
HStack(spacing: 16) {
Image(systemName: "externaldrive.connected.to.line.below")
.font(.system(size: 23))
.foregroundStyle(.blue)
Image(nsImage: NSImage(named: "WorkbenchBrand") ?? NSImage(contentsOfFile: Bundle.main.path(forResource: "WorkbenchBrand", ofType: "png") ?? "") ?? NSImage())
.resizable()
.scaledToFit()
.frame(width: 30, height: 30)
Text("MySQL Workbench")
.font(.system(size: 15, weight: .semibold))
@@ -2540,31 +2541,8 @@ struct WorkbenchAdministrationView: View {
private var resultContent: some View {
switch page {
case .serverCounters:
// Keep live charts permanently visible while the complete status table scrolls.
// Previously both were inside a single ScrollView, so scrolling to the
// counters made it appear as though the graphs had disappeared.
VStack(alignment: .leading, spacing: 10) {
HStack {
Label("Live Server Counters", systemImage: "chart.xyaxis.line")
.font(.title2.bold())
Spacer()
Text("\(diagnosticSamples.count) samples · updates every 5 seconds")
.font(.caption).foregroundStyle(.secondary)
}
WorkbenchPinnedCounterGraphs(samples: diagnosticSamples)
.frame(height: 222)
HStack {
Text("All MySQL server statistics")
.font(.headline)
Spacer()
Text("\(filteredRows.count) counters")
.font(.caption).foregroundStyle(.secondary)
}
WorkbenchColouredCounterTable(columns: result.columns, rows: filteredRows)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.padding(14)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
WorkbenchServerCountersScreen(samples: diagnosticSamples, rows: filteredRows)
.frame(maxWidth: .infinity, maxHeight: .infinity)
case .dashboard:
ScrollView {
VStack(alignment: .leading, spacing: 18) {
@@ -4177,7 +4155,7 @@ private struct WorkbenchInlinePerformanceDashboard: View {
Text("\(samples.count) samples · refreshes every 5 seconds")
.font(.caption).foregroundStyle(.secondary)
}
LazyVGrid(columns: [GridItem(.adaptive(minimum: 270), spacing: 12)], spacing: 12) {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 360), spacing: 14)], 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)
@@ -4191,6 +4169,81 @@ private struct WorkbenchInlinePerformanceDashboard: View {
}
}
// MARK: - Phase 34: Responsive server-counter workspace
private struct WorkbenchServerCountersScreen: View {
let samples: [DiagnosticSample]
let rows: [[String]]
@State private var category = "All"
private let categories = ["All", "Connections", "Queries", "InnoDB", "Network", "Errors", "Replication", "Other"]
private func group(_ name: String) -> String {
let n = name.lowercased()
if n.hasPrefix("innodb_") { return "InnoDB" }
if n.contains("replica") || n.contains("slave") || n.contains("binlog") || n.contains("relay_log") { return "Replication" }
if n.contains("abort") || n.contains("error") || n.contains("failed") || n.contains("denied") { return "Errors" }
if n.contains("bytes_") || n.contains("network") { return "Network" }
if n.contains("connection") || n.contains("thread") || n.contains("client") { return "Connections" }
if n.contains("quer") || n.contains("question") || n.hasPrefix("com_") || n.contains("select") || n.contains("sort") || n.contains("tmp_") { return "Queries" }
return "Other"
}
private func value(_ name: String) -> String {
guard let row = rows.first(where: { $0.count >= 2 && $0[0].caseInsensitiveCompare(name) == .orderedSame }) else { return "—" }
return WorkbenchCounterCharts.displayValue(row[1], for: name)
}
private func overview(_ title: String, metric: String, tint: Color) -> some View {
VStack(alignment: .leading, spacing: 6) {
Text(title).font(.caption.weight(.medium)).foregroundStyle(.secondary)
Text(value(metric)).font(.system(size: 23, weight: .bold, design: .rounded))
.foregroundStyle(tint).lineLimit(1).minimumScaleFactor(0.7)
}
.frame(maxWidth: .infinity, minHeight: 66, alignment: .leading)
.padding(12)
.background(tint.opacity(0.08), in: RoundedRectangle(cornerRadius: 10))
}
private var visibleRows: [[String]] {
category == "All" ? rows : rows.filter { $0.count >= 2 && group($0[0]) == category }
}
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 18) {
HStack {
Label("Server overview", systemImage: "server.rack").font(.title3.bold())
Spacer()
Text("Live MySQL status · 5-second refresh").font(.caption).foregroundStyle(.secondary)
}
LazyVGrid(columns: [GridItem(.adaptive(minimum: 155), spacing: 10)], spacing: 10) {
overview("Queries", metric: "Queries", tint: .blue)
overview("Connected", metric: "Threads_connected", tint: .teal)
overview("Running", metric: "Threads_running", tint: .orange)
overview("Slow queries", metric: "Slow_queries", tint: .red)
overview("Received", metric: "Bytes_received", tint: .purple)
overview("Sent", metric: "Bytes_sent", tint: .green)
}
HStack {
Label("Performance trends", systemImage: "chart.xyaxis.line").font(.title3.bold())
Spacer()
Text("\(samples.count) samples").font(.caption).foregroundStyle(.secondary)
}
WorkbenchCounterCharts(page: .serverCounters, samples: samples)
HStack {
Label("All server counters", systemImage: "list.bullet.rectangle").font(.title3.bold())
Spacer()
Text("\(visibleRows.count) of \(rows.count)").font(.caption).foregroundStyle(.secondary)
}
Picker("Counter category", selection: $category) {
ForEach(categories, id: \.self) { Text($0).tag($0) }
}
.pickerStyle(.segmented)
WorkbenchColouredCounterTable(columns: ["Variable_name", "Value"], rows: visibleRows)
.frame(minHeight: 360, idealHeight: 600)
Text("Colours identify counter families, not server health. Error totals are cumulative; a nonzero value does not by itself indicate a current fault.")
.font(.caption).foregroundStyle(.secondary)
}
.padding(18)
}
}
}
// MARK: - Phase 29: Inline coloured counter graphs
// MARK: - Phase 33: pinned server graphs and complete coloured statistics
private struct WorkbenchPinnedCounterGraphs: View {
@@ -4416,7 +4469,7 @@ private struct WorkbenchCounterCharts: View {
Text("\(samples.count) samples · 5-second refresh")
.font(.caption).foregroundStyle(.secondary)
}
LazyVGrid(columns: [GridItem(.adaptive(minimum: 270), spacing: 12)], spacing: 12) {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 360), spacing: 14)], spacing: 12) {
ForEach(metrics, id: \.self) { metric in graph(metric) }
}
Text("Graphs use real MySQL samples. Cumulative counters display change per second; thread counts show current values. Rates appear after two successful samples.")