diff --git a/WorkbenchReplacement/MainView.swift b/WorkbenchReplacement/MainView.swift index ad5d9f5..b956ac0 100644 --- a/WorkbenchReplacement/MainView.swift +++ b/WorkbenchReplacement/MainView.swift @@ -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 {