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:
Generated
+1
@@ -2,6 +2,7 @@
|
||||
<module type="EMPTY_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/build" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/Build/Intermediates.noindex/EagerLinkingTBDs" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/Build/Intermediates.noindex/PrecompiledHeaders" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/Build/Intermediates.noindex/XCBuildData/120e9a8ae12faa2f3ba3b63c6df49ba3.xcbuilddata" />
|
||||
|
||||
@@ -323,6 +323,7 @@ struct MainView: View {
|
||||
"MANAGEMENT",
|
||||
pages: [
|
||||
.serverStatus,
|
||||
.serverCounters,
|
||||
.clientConnections,
|
||||
.users,
|
||||
.variables,
|
||||
@@ -2060,6 +2061,7 @@ struct NativeResultGrid: NSViewRepresentable {
|
||||
|
||||
enum WorkbenchAdminPage: String, CaseIterable, Identifiable {
|
||||
case serverStatus = "Server Status"
|
||||
case serverCounters = "Server Counters"
|
||||
case clientConnections = "Client Connections"
|
||||
case users = "Users and Privileges"
|
||||
case variables = "Status and System Variables"
|
||||
@@ -2084,6 +2086,7 @@ enum WorkbenchAdminPage: String, CaseIterable, Identifiable {
|
||||
case .connectionStatistics: return "network"
|
||||
case .serverLogs: return "text.alignleft"
|
||||
case .serverStatus: return "server.rack"
|
||||
case .serverCounters: return "chart.xyaxis.line"
|
||||
case .clientConnections: return "person.2"
|
||||
case .users: return "person.crop.circle.badge.checkmark"
|
||||
case .variables: return "slider.horizontal.3"
|
||||
@@ -2144,19 +2147,10 @@ enum WorkbenchAdminPage: String, CaseIterable, Identifiable {
|
||||
"""
|
||||
case .serverLogs:
|
||||
return "SELECT LOGGED, PRIO, ERROR_CODE, SUBSYSTEM, DATA FROM performance_schema.error_log ORDER BY LOGGED DESC LIMIT 500"
|
||||
case .serverCounters:
|
||||
return "SHOW GLOBAL STATUS"
|
||||
case .serverStatus:
|
||||
return """
|
||||
SHOW GLOBAL STATUS WHERE Variable_name IN (
|
||||
'Uptime',
|
||||
'Threads_connected',
|
||||
'Threads_running',
|
||||
'Questions',
|
||||
'Queries',
|
||||
'Connections',
|
||||
'Bytes_received',
|
||||
'Bytes_sent'
|
||||
)
|
||||
"""
|
||||
return "SHOW GLOBAL STATUS"
|
||||
|
||||
case .clientConnections:
|
||||
return "SHOW FULL PROCESSLIST"
|
||||
@@ -2237,7 +2231,7 @@ struct WorkbenchAdministrationView: View {
|
||||
|
||||
private var supportsTrends: Bool {
|
||||
page == .slowQueries || page == .connectionStatistics ||
|
||||
page == .innodbMetrics || page == .dashboard || page == .serverStatus || page == .variables
|
||||
page == .innodbMetrics || page == .dashboard || page == .serverStatus || page == .serverCounters
|
||||
}
|
||||
|
||||
private var diagnosticGuidance: String? {
|
||||
@@ -2514,7 +2508,7 @@ struct WorkbenchAdministrationView: View {
|
||||
|
||||
Spacer()
|
||||
|
||||
if page != .dashboard && page != .serverStatus {
|
||||
if page != .dashboard {
|
||||
TextField("Filter results", text: $searchText)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.frame(width: 175)
|
||||
@@ -2540,78 +2534,73 @@ struct WorkbenchAdministrationView: View {
|
||||
.padding(18)
|
||||
}
|
||||
|
||||
// Keep the administration pages distinct: the complete server-status table and
|
||||
// system-variable grid must never be replaced by a dashboard of metric cards.
|
||||
@ViewBuilder
|
||||
private var resultContent: some View {
|
||||
if page == .dashboard || page == .serverStatus || page == .connectionStatistics || page == .innodbMetrics || page == .variables {
|
||||
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)
|
||||
case .dashboard:
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 18) {
|
||||
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: [
|
||||
GridItem(.adaptive(minimum: 180), spacing: 12)
|
||||
],
|
||||
spacing: 12
|
||||
) {
|
||||
ForEach(result.rows.indices, id: \.self) { index in
|
||||
let row = result.rows[index]
|
||||
|
||||
if row.count >= 2 {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text(
|
||||
row[0]
|
||||
.replacingOccurrences(
|
||||
of: "_",
|
||||
with: " "
|
||||
)
|
||||
)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
Text(row[1])
|
||||
.foregroundStyle(WorkbenchCounterCharts.tint(for: row[0]))
|
||||
.font(
|
||||
.system(
|
||||
size: 23,
|
||||
weight: .semibold,
|
||||
design: .rounded
|
||||
)
|
||||
)
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.65)
|
||||
}
|
||||
.frame(
|
||||
maxWidth: .infinity,
|
||||
minHeight: 90,
|
||||
alignment: .leading
|
||||
)
|
||||
.padding(14)
|
||||
.background(
|
||||
WorkbenchCounterCharts.tint(for: row[0]).opacity(0.09)
|
||||
)
|
||||
.clipShape(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
WorkbenchInlinePerformanceDashboard(samples: diagnosticSamples)
|
||||
Text("Server statistics").font(.title2.bold())
|
||||
NativeResultGrid(columns: result.columns, rows: filteredRows)
|
||||
.frame(minHeight: 340)
|
||||
}
|
||||
.padding(18)
|
||||
}
|
||||
} else {
|
||||
NativeResultGrid(
|
||||
columns: result.columns,
|
||||
rows: filteredRows
|
||||
)
|
||||
.frame(
|
||||
maxWidth: .infinity,
|
||||
maxHeight: .infinity
|
||||
)
|
||||
case .serverStatus:
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 18) {
|
||||
Text("Server Status").font(.title2.bold())
|
||||
WorkbenchInlinePerformanceDashboard(samples: diagnosticSamples)
|
||||
Text("Server statistics").font(.title2.bold())
|
||||
NativeResultGrid(columns: result.columns, rows: filteredRows)
|
||||
.frame(minHeight: 420)
|
||||
}
|
||||
.padding(18)
|
||||
}
|
||||
case .connectionStatistics, .innodbMetrics:
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 18) {
|
||||
WorkbenchCounterCharts(page: page, samples: diagnosticSamples)
|
||||
Text("All statistics").font(.title2.bold())
|
||||
NativeResultGrid(columns: result.columns, rows: filteredRows)
|
||||
.frame(minHeight: 440)
|
||||
}
|
||||
.padding(18)
|
||||
}
|
||||
default:
|
||||
// Status and System Variables and all other administration screens
|
||||
// retain their original searchable tabular presentation.
|
||||
NativeResultGrid(columns: result.columns, rows: filteredRows)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4203,6 +4192,122 @@ private struct WorkbenchInlinePerformanceDashboard: View {
|
||||
}
|
||||
|
||||
// MARK: - Phase 29: Inline coloured counter graphs
|
||||
// MARK: - Phase 33: pinned server graphs and complete coloured statistics
|
||||
private struct WorkbenchPinnedCounterGraphs: View {
|
||||
let samples: [DiagnosticSample]
|
||||
private let metrics = ["Queries", "Questions", "Threads_connected", "Threads_running", "Slow_queries", "Bytes_received", "Bytes_sent", "Innodb_buffer_pool_reads", "Innodb_data_reads"]
|
||||
|
||||
private func sampleValue(_ sample: DiagnosticSample, metric: String) -> Double? {
|
||||
guard let row = sample.rows.first(where: { $0.count > 1 && $0[0].caseInsensitiveCompare(metric) == .orderedSame }) else { return nil }
|
||||
return Double(row[1])
|
||||
}
|
||||
|
||||
private func values(for metric: String) -> [CounterGraphPoint] {
|
||||
samples.indices.compactMap { index in
|
||||
guard let current = sampleValue(samples[index], metric: metric) else { return nil }
|
||||
if metric.hasPrefix("Threads_") {
|
||||
return CounterGraphPoint(time: samples[index].at, value: current)
|
||||
}
|
||||
guard index > 0, let prior = sampleValue(samples[index - 1], metric: metric), current >= prior else { return nil }
|
||||
let seconds = samples[index].at.timeIntervalSince(samples[index - 1].at)
|
||||
guard seconds > 0 else { return nil }
|
||||
return CounterGraphPoint(time: samples[index].at, value: (current - prior) / seconds)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView(.horizontal) {
|
||||
HStack(spacing: 12) {
|
||||
ForEach(metrics, id: \.self) { metric in
|
||||
let points = values(for: metric)
|
||||
let colour = WorkbenchCounterCharts.tint(for: metric)
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(metric.replacingOccurrences(of: "_", with: " "))
|
||||
.font(.subheadline.bold())
|
||||
HStack(alignment: .firstTextBaseline) {
|
||||
Text(points.last.map { $0.value.formatted(.number.precision(.fractionLength(1))) } ?? "—")
|
||||
.font(.title2.bold()).foregroundStyle(colour)
|
||||
Text(metric.hasPrefix("Threads_") ? "current" : "/sec")
|
||||
.font(.caption).foregroundStyle(.secondary)
|
||||
}
|
||||
Chart(points) { point in
|
||||
AreaMark(x: .value("Time", point.time), y: .value("Value", point.value))
|
||||
.foregroundStyle(colour.opacity(0.18))
|
||||
LineMark(x: .value("Time", point.time), y: .value("Value", point.value))
|
||||
.foregroundStyle(colour)
|
||||
.lineStyle(StrokeStyle(lineWidth: 2.5))
|
||||
}
|
||||
.chartYAxis { AxisMarks(position: .leading) }
|
||||
.frame(height: 105)
|
||||
if points.isEmpty {
|
||||
Text("Waiting for samples")
|
||||
.font(.caption2).foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
.padding(12)
|
||||
.frame(width: 290, height: 200, alignment: .topLeading)
|
||||
.background(colour.opacity(0.09))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct CounterGraphPoint: Identifiable {
|
||||
let id = UUID()
|
||||
let time: Date
|
||||
let value: Double
|
||||
}
|
||||
|
||||
private struct WorkbenchColouredCounterTable: View {
|
||||
let columns: [String]
|
||||
let rows: [[String]]
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
HStack {
|
||||
Text("Variable name").frame(maxWidth: .infinity, alignment: .leading)
|
||||
Text("Value").frame(width: 210, alignment: .trailing)
|
||||
}
|
||||
.font(.subheadline.bold())
|
||||
.padding(.horizontal, 12).padding(.vertical, 10)
|
||||
.background(Color.secondary.opacity(0.09))
|
||||
Divider()
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 0) {
|
||||
ForEach(rows.indices, id: \.self) { index in
|
||||
let row = rows[index]
|
||||
if row.count >= 2 {
|
||||
let colour = WorkbenchCounterCharts.tint(for: row[0])
|
||||
HStack(spacing: 10) {
|
||||
RoundedRectangle(cornerRadius: 2)
|
||||
.fill(colour).frame(width: 4, height: 22)
|
||||
Text(row[0])
|
||||
.font(.system(.body, design: .monospaced))
|
||||
.lineLimit(1)
|
||||
.help(row[0])
|
||||
Spacer(minLength: 8)
|
||||
Text(WorkbenchCounterCharts.displayValue(row[1], for: row[0]))
|
||||
.font(.system(.body, design: .monospaced).weight(.semibold))
|
||||
.foregroundStyle(colour)
|
||||
.lineLimit(1)
|
||||
.help(row[1])
|
||||
}
|
||||
.padding(.horizontal, 12).padding(.vertical, 7)
|
||||
.background(index.isMultiple(of: 2) ? Color.secondary.opacity(0.045) : Color.clear)
|
||||
Divider().opacity(0.35)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
.overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.secondary.opacity(0.18)))
|
||||
}
|
||||
}
|
||||
|
||||
private struct WorkbenchCounterCharts: View {
|
||||
let page: WorkbenchAdminPage
|
||||
let samples: [DiagnosticSample]
|
||||
@@ -4223,12 +4328,30 @@ private struct WorkbenchCounterCharts: View {
|
||||
return .blue
|
||||
}
|
||||
|
||||
static func displayValue(_ raw: String, for name: String) -> String {
|
||||
guard let number = Double(raw), number.isFinite else { return raw }
|
||||
let key = name.lowercased()
|
||||
if key == "uptime" {
|
||||
let seconds = max(0, Int(number))
|
||||
let days = seconds / 86400
|
||||
let hours = (seconds % 86400) / 3600
|
||||
let minutes = (seconds % 3600) / 60
|
||||
return days > 0 ? "\(days)d \(hours)h \(minutes)m" : "\(hours)h \(minutes)m"
|
||||
}
|
||||
if key.contains("bytes") {
|
||||
return ByteCountFormatter.string(fromByteCount: Int64(number), countStyle: .binary)
|
||||
}
|
||||
return number.formatted(.number.grouping(.automatic).precision(.fractionLength(0...2)))
|
||||
}
|
||||
|
||||
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 .serverCounters:
|
||||
return ["Queries", "Questions", "Threads_connected", "Threads_running", "Slow_queries", "Bytes_received", "Bytes_sent", "Innodb_buffer_pool_reads", "Innodb_data_reads"]
|
||||
case .variables:
|
||||
return ["Queries", "Threads_connected", "Threads_running", "Connections", "Slow_queries", "Bytes_sent"]
|
||||
default:
|
||||
@@ -4287,7 +4410,7 @@ private struct WorkbenchCounterCharts: View {
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
HStack {
|
||||
Label("Live counter graphs", systemImage: "chart.xyaxis.line")
|
||||
Label(page == .serverCounters ? "Server Counters · Live Graphs" : "Live counter graphs", systemImage: "chart.xyaxis.line")
|
||||
.font(.title2.bold())
|
||||
Spacer()
|
||||
Text("\(samples.count) samples · 5-second refresh")
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# Phase 30 — Server Counters graphs fix
|
||||
|
||||
- Adds a dedicated **Server Counters** item under **Administration → Management**.
|
||||
- Retrieves all actual server counters using `SHOW GLOBAL STATUS` (not `SHOW GLOBAL VARIABLES`).
|
||||
- Places nine colour-coded Swift Charts above the counters, directly on the Server Counters screen.
|
||||
- Graphs show current thread counts and per-second rates for cumulative counters.
|
||||
- Counter cards remain visible beneath the charts; the filter field filters the cards.
|
||||
- Samples are collected every five seconds when Auto-refresh is enabled. The first rate point requires two successful samples.
|
||||
- Existing dashboard, SQL editor and schema editor are preserved.
|
||||
|
||||
This release was not compiled on macOS or tested against a live MySQL server.
|
||||
@@ -0,0 +1,7 @@
|
||||
# Phase 31 — Server Counters Layout
|
||||
|
||||
- Server counter cards appear first, with a compact responsive grid.
|
||||
- Live graphs appear directly below the counter cards in the same scroll view.
|
||||
- Bytes counters are displayed in readable units; uptime in days/hours/minutes; numeric counters use separators.
|
||||
- Existing graph collection, automatic refresh and schema editing are retained.
|
||||
- Requires macOS/Xcode compilation and live MySQL verification.
|
||||
@@ -0,0 +1,8 @@
|
||||
# Phase 32 — Administration View Restoration
|
||||
|
||||
- Server Counters: inline coloured graphs followed by the complete searchable MySQL status grid (no omitted counters).
|
||||
- Server Status: full SHOW GLOBAL STATUS data restored below the performance overview, with search.
|
||||
- Status and System Variables: restored original native table/grid presentation; no inappropriate charts or counter tiles.
|
||||
- Connection Statistics and InnoDB: retain graphs and complete tabular results.
|
||||
- No MySQL mutations; monitoring remains read-only.
|
||||
- Xcode build and live MySQL validation must be performed on macOS.
|
||||
@@ -0,0 +1 @@
|
||||
Phase 33: Server Counters fixed split layout. Nine real MySQL sample charts pinned in horizontal scroll strip above independently scrolling full coloured SHOW GLOBAL STATUS table. All counters retained. Auto-refresh does not blank existing data. No Xcode build performed.
|
||||
Reference in New Issue
Block a user