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:
@@ -2211,6 +2211,14 @@ struct WorkbenchAdministrationView: View {
|
||||
@State private var processActionError: String? = nil
|
||||
@State private var processActionStatus: String? = nil
|
||||
@State private var terminatingProcess = false
|
||||
// Phase 25/26: bounded, in-session samples. No credentials or SQL persisted.
|
||||
@State private var diagnosticSamples: [DiagnosticSample] = []
|
||||
@State private var showingTrends = false
|
||||
|
||||
private var supportsTrends: Bool {
|
||||
page == .slowQueries || page == .connectionStatistics ||
|
||||
page == .innodbMetrics || page == .dashboard || page == .serverStatus
|
||||
}
|
||||
|
||||
private var diagnosticGuidance: String? {
|
||||
switch page {
|
||||
@@ -2284,6 +2292,20 @@ struct WorkbenchAdministrationView: View {
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
|
||||
if supportsTrends && !diagnosticSamples.isEmpty {
|
||||
HStack(spacing: 12) {
|
||||
Label("\(diagnosticSamples.count) samples", systemImage: "chart.xyaxis.line")
|
||||
.foregroundStyle(.secondary)
|
||||
Button("View Trends…") { showingTrends = true }
|
||||
Button("Clear Samples") { diagnosticSamples.removeAll() }
|
||||
Spacer()
|
||||
Text("Samples are kept in memory for this screen only")
|
||||
.font(.caption).foregroundStyle(.secondary)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 6)
|
||||
}
|
||||
|
||||
if page == .clientConnections {
|
||||
processControls
|
||||
}
|
||||
@@ -2361,6 +2383,9 @@ struct WorkbenchAdministrationView: View {
|
||||
)
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showingTrends) {
|
||||
WorkbenchDiagnosticTrendsView(page: page, samples: diagnosticSamples)
|
||||
}
|
||||
.sheet(isPresented: $showingGrants) {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("Grants: \(selectedAccount ?? "")")
|
||||
@@ -2626,6 +2651,12 @@ struct WorkbenchAdministrationView: View {
|
||||
)
|
||||
|
||||
result = response
|
||||
if supportsTrends {
|
||||
diagnosticSamples.append(DiagnosticSample(at: Date(), columns: response.columns, rows: response.rows))
|
||||
if diagnosticSamples.count > 60 {
|
||||
diagnosticSamples.removeFirst(diagnosticSamples.count - 60)
|
||||
}
|
||||
}
|
||||
if page == .clientConnections,
|
||||
let selectedProcessID,
|
||||
!processIDs.contains(selectedProcessID) {
|
||||
@@ -4055,3 +4086,93 @@ private struct WorkbenchAccountManagerView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Phase 25/26: Live performance samples and counter rates
|
||||
|
||||
private struct DiagnosticSample: Identifiable {
|
||||
let id = UUID()
|
||||
let at: Date
|
||||
let columns: [String]
|
||||
let rows: [[String]]
|
||||
}
|
||||
|
||||
private struct WorkbenchDiagnosticTrendsView: View {
|
||||
let page: WorkbenchAdminPage
|
||||
let samples: [DiagnosticSample]
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
private func metricMap(_ sample: DiagnosticSample) -> [String: Double] {
|
||||
var values: [String: Double] = [:]
|
||||
if page == .slowQueries {
|
||||
guard let name = sample.columns.firstIndex(of: "DIGEST_TEXT"),
|
||||
let count = sample.columns.firstIndex(of: "COUNT_STAR") else { return values }
|
||||
for row in sample.rows where row.indices.contains(name) && row.indices.contains(count) {
|
||||
if let value = Double(row[count]) { values[row[name]] = value }
|
||||
}
|
||||
} else {
|
||||
for row in sample.rows where row.count >= 2 {
|
||||
if let value = Double(row[1]) { values[row[0]] = value }
|
||||
}
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
private var comparisons: [(String, Double, Double, Double?)] {
|
||||
guard let latest = samples.last else { return [] }
|
||||
let current = metricMap(latest)
|
||||
let prior = samples.count > 1 ? metricMap(samples[samples.count - 2]) : [:]
|
||||
let seconds = samples.count > 1
|
||||
? latest.at.timeIntervalSince(samples[samples.count - 2].at) : 0
|
||||
return current.keys.sorted().map { key in
|
||||
let value = current[key] ?? 0
|
||||
let before = prior[key] ?? value
|
||||
let rate: Double? = seconds > 0 && prior[key] != nil && value >= before
|
||||
? (value - before) / seconds : nil
|
||||
return (key, value, before, rate)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
HStack {
|
||||
Text("\(page.rawValue) — Trends").font(.title2.bold())
|
||||
Spacer()
|
||||
Button("Close") { dismiss() }
|
||||
}
|
||||
Text("\(samples.count) samples. Rates compare the two latest successful refreshes; cumulative counters can reset when MySQL restarts or statistics are cleared.")
|
||||
.font(.caption).foregroundStyle(.secondary)
|
||||
if samples.count < 2 {
|
||||
ContentUnavailableView("More samples needed", systemImage: "chart.xyaxis.line",
|
||||
description: Text("Refresh this screen twice, or enable auto-refresh."))
|
||||
} else {
|
||||
ScrollView {
|
||||
LazyVStack(alignment: .leading, spacing: 8) {
|
||||
HStack {
|
||||
Text("Metric").frame(maxWidth: .infinity, alignment: .leading)
|
||||
Text("Current").frame(width: 110, alignment: .trailing)
|
||||
Text("Previous").frame(width: 110, alignment: .trailing)
|
||||
Text("Change/sec").frame(width: 110, alignment: .trailing)
|
||||
}.font(.headline)
|
||||
Divider()
|
||||
ForEach(comparisons.indices, id: \.self) { index in
|
||||
let item = comparisons[index]
|
||||
HStack {
|
||||
Text(item.0).lineLimit(2).frame(maxWidth: .infinity, alignment: .leading)
|
||||
Text(item.1.formatted()).frame(width: 110, alignment: .trailing)
|
||||
Text(item.2.formatted()).frame(width: 110, alignment: .trailing)
|
||||
Text(item.3.map { $0.formatted(.number.precision(.fractionLength(2))) } ?? "—")
|
||||
.frame(width: 110, alignment: .trailing)
|
||||
}.font(.system(.caption, design: .monospaced))
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Text("Query digest counts are grouped by SQL fingerprint. This is a sampled comparison, not a durable monitoring database.")
|
||||
.font(.caption).foregroundStyle(.secondary)
|
||||
}
|
||||
.padding(20)
|
||||
.frame(minWidth: 820, minHeight: 480)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user