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:
@@ -2237,7 +2237,7 @@ struct WorkbenchAdministrationView: View {
|
||||
|
||||
private var supportsTrends: Bool {
|
||||
page == .slowQueries || page == .connectionStatistics ||
|
||||
page == .innodbMetrics || page == .dashboard || page == .serverStatus
|
||||
page == .innodbMetrics || page == .dashboard || page == .serverStatus || page == .variables
|
||||
}
|
||||
|
||||
private var diagnosticGuidance: String? {
|
||||
@@ -2542,11 +2542,15 @@ struct WorkbenchAdministrationView: View {
|
||||
|
||||
@ViewBuilder
|
||||
private var resultContent: some View {
|
||||
if page == .dashboard || page == .serverStatus {
|
||||
if page == .dashboard || page == .serverStatus || page == .connectionStatistics || page == .innodbMetrics || page == .variables {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 18) {
|
||||
WorkbenchInlinePerformanceDashboard(samples: diagnosticSamples)
|
||||
Text("Server counters")
|
||||
if page == .dashboard || page == .serverStatus {
|
||||
WorkbenchInlinePerformanceDashboard(samples: diagnosticSamples)
|
||||
} else {
|
||||
WorkbenchCounterCharts(page: page, samples: diagnosticSamples)
|
||||
}
|
||||
Text(page == .innodbMetrics ? "InnoDB counters" : page == .variables ? "Server variables and counters" : "Server counters")
|
||||
.font(.headline)
|
||||
LazyVGrid(
|
||||
columns: [
|
||||
@@ -2570,6 +2574,7 @@ struct WorkbenchAdministrationView: View {
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
Text(row[1])
|
||||
.foregroundStyle(WorkbenchCounterCharts.tint(for: row[0]))
|
||||
.font(
|
||||
.system(
|
||||
size: 23,
|
||||
@@ -2587,7 +2592,7 @@ struct WorkbenchAdministrationView: View {
|
||||
)
|
||||
.padding(14)
|
||||
.background(
|
||||
Color(nsColor: .controlBackgroundColor)
|
||||
WorkbenchCounterCharts.tint(for: row[0]).opacity(0.09)
|
||||
)
|
||||
.clipShape(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
@@ -4197,6 +4202,106 @@ private struct WorkbenchInlinePerformanceDashboard: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Phase 29: Inline coloured counter graphs
|
||||
private struct WorkbenchCounterCharts: View {
|
||||
let page: WorkbenchAdminPage
|
||||
let samples: [DiagnosticSample]
|
||||
|
||||
private struct Point: Identifiable {
|
||||
let id: Int
|
||||
let time: Date
|
||||
let value: Double
|
||||
}
|
||||
|
||||
static func tint(for name: String) -> Color {
|
||||
let key = name.lowercased()
|
||||
if key.contains("abort") || key.contains("error") || key.contains("slow") || key.contains("wait") { return .red }
|
||||
if key.contains("thread") || key.contains("connection") { return .teal }
|
||||
if key.contains("innodb") || key.contains("buffer") { return .purple }
|
||||
if key.contains("bytes") || key.contains("network") { return .green }
|
||||
if key.contains("disk") || key.contains("tmp") { return .orange }
|
||||
return .blue
|
||||
}
|
||||
|
||||
private var metrics: [String] {
|
||||
switch page {
|
||||
case .innodbMetrics:
|
||||
return ["Innodb_buffer_pool_reads", "Innodb_buffer_pool_read_requests", "Innodb_data_reads", "Innodb_data_writes", "Innodb_rows_read", "Innodb_row_lock_waits"]
|
||||
case .connectionStatistics:
|
||||
return ["Threads_connected", "Threads_running", "Connections", "Queries", "Slow_queries", "Aborted_connects"]
|
||||
case .variables:
|
||||
return ["Queries", "Threads_connected", "Threads_running", "Connections", "Slow_queries", "Bytes_sent"]
|
||||
default:
|
||||
return ["Queries", "Threads_connected", "Threads_running", "Slow_queries"]
|
||||
}
|
||||
}
|
||||
|
||||
private func value(_ sample: DiagnosticSample, _ metric: String) -> Double? {
|
||||
guard let row = sample.rows.first(where: { $0.count >= 2 && $0[0].caseInsensitiveCompare(metric) == .orderedSame }) else { return nil }
|
||||
return Double(row[1])
|
||||
}
|
||||
|
||||
private func points(_ metric: String, rate: Bool) -> [Point] {
|
||||
samples.indices.compactMap { i in
|
||||
guard let current = value(samples[i], metric), current.isFinite else { return nil }
|
||||
if !rate { return Point(id: i, time: samples[i].at, value: current) }
|
||||
guard i > 0, let previous = value(samples[i - 1], metric), current >= previous else { return nil }
|
||||
let elapsed = samples[i].at.timeIntervalSince(samples[i - 1].at)
|
||||
guard elapsed > 0 else { return nil }
|
||||
return Point(id: i, time: samples[i].at, value: (current - previous) / elapsed)
|
||||
}
|
||||
}
|
||||
|
||||
private func graph(_ metric: String) -> some View {
|
||||
let instantaneous = metric.hasPrefix("Threads_")
|
||||
let values = points(metric, rate: !instantaneous)
|
||||
let tint = Self.tint(for: metric)
|
||||
return VStack(alignment: .leading, spacing: 8) {
|
||||
Text(metric.replacingOccurrences(of: "_", with: " ")).font(.subheadline.weight(.semibold))
|
||||
.lineLimit(1).minimumScaleFactor(0.8)
|
||||
HStack(alignment: .firstTextBaseline) {
|
||||
Text(values.last.map { $0.value.formatted(.number.precision(.fractionLength(1))) } ?? "—")
|
||||
.font(.system(size: 23, weight: .bold, design: .rounded))
|
||||
.foregroundStyle(tint)
|
||||
Text(instantaneous ? "current" : "/sec")
|
||||
.font(.caption).foregroundStyle(.secondary)
|
||||
}
|
||||
Chart(values) { point in
|
||||
AreaMark(x: .value("Time", point.time), y: .value("Value", point.value))
|
||||
.foregroundStyle(tint.opacity(0.13))
|
||||
LineMark(x: .value("Time", point.time), y: .value("Value", point.value))
|
||||
.foregroundStyle(tint).lineStyle(StrokeStyle(lineWidth: 2.5))
|
||||
}
|
||||
.chartYAxis { AxisMarks(position: .leading) }
|
||||
.frame(height: 115)
|
||||
if values.isEmpty {
|
||||
Text("Waiting for samples").font(.caption2).foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
.padding(14)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(tint.opacity(0.08))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
HStack {
|
||||
Label("Live counter graphs", systemImage: "chart.xyaxis.line")
|
||||
.font(.title2.bold())
|
||||
Spacer()
|
||||
Text("\(samples.count) samples · 5-second refresh")
|
||||
.font(.caption).foregroundStyle(.secondary)
|
||||
}
|
||||
LazyVGrid(columns: [GridItem(.adaptive(minimum: 270), spacing: 12)], 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.")
|
||||
.font(.caption).foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Phase 25/26: Live performance samples and counter rates
|
||||
|
||||
private struct DiagnosticSample: Identifiable {
|
||||
|
||||
Reference in New Issue
Block a user