This commit is contained in:
Mark Gregory
2026-10-16 21:35:47 +01:00
parent 773191f206
commit 7387be60b5
22 changed files with 575 additions and 32 deletions
+398 -2
View File
@@ -33,6 +33,10 @@ struct MainView: View {
@State private var selectedTableName: String? = nil
@State private var outputExpanded = true
@State private var navigatorMode = 0
@State private var administrationPage: WorkbenchAdminPage? = nil
var body: some View {
VStack(spacing: 0) {
mainToolbar
@@ -163,8 +167,27 @@ struct MainView: View {
private var navigator: some View {
VStack(spacing: 0) {
sectionHeader("NAVIGATOR")
Picker("Navigator", selection: $navigatorMode) {
Text("Administration").tag(0)
Text("Schemas").tag(1)
}
.pickerStyle(.segmented)
.labelsHidden()
.padding(8)
if navigatorMode == 0 {
administrationNavigator
} else {
schemasNavigator
}
}
.background(WB.sidebar)
}
private var schemasNavigator: some View {
VStack(spacing: 0) {
HStack {
Text("SCHEMAS")
.font(.system(size: 11, weight: .bold))
@@ -230,7 +253,67 @@ struct MainView: View {
}
.frame(height: 125)
}
.background(WB.sidebar)
}
private var administrationNavigator: some View {
ScrollView {
VStack(alignment: .leading, spacing: 4) {
administrationSection(
"MANAGEMENT",
pages: [
.serverStatus,
.clientConnections,
.users,
.variables
]
)
administrationSection(
"PERFORMANCE",
pages: [
.dashboard,
.performanceReports,
.performanceSchema
]
)
}
.padding(8)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
private func administrationSection(
_ title: String,
pages: [WorkbenchAdminPage]
) -> some View {
VStack(alignment: .leading, spacing: 4) {
Text(title)
.font(.system(size: 11, weight: .bold))
.foregroundStyle(.secondary)
.padding(.top, 12)
.padding(.bottom, 4)
ForEach(pages) { page in
Button {
administrationPage = page
} label: {
Label(page.rawValue, systemImage: page.icon)
.frame(
maxWidth: .infinity,
alignment: .leading
)
.padding(.horizontal, 8)
.padding(.vertical, 7)
.background(
administrationPage == page
? Color.accentColor.opacity(0.18)
: Color.clear
)
.clipShape(RoundedRectangle(cornerRadius: 5))
}
.buttonStyle(.plain)
}
}
}
@@ -444,6 +527,22 @@ struct MainView: View {
// MARK: - Workspace
private var workspace: some View {
Group {
if let page = administrationPage {
WorkbenchAdministrationView(
page: page,
profile: model.selected
) {
administrationPage = nil
navigatorMode = 1
}
} else {
sqlWorkspace
}
}
}
private var sqlWorkspace: some View {
VStack(spacing: 0) {
queryTabBar
@@ -1143,3 +1242,300 @@ struct NativeResultGrid: NSViewRepresentable {
}
}
}
// MARK: - MySQL Administration
enum WorkbenchAdminPage: String, CaseIterable, Identifiable {
case serverStatus = "Server Status"
case clientConnections = "Client Connections"
case users = "Users and Privileges"
case variables = "Status and System Variables"
case dashboard = "Dashboard"
case performanceReports = "Performance Reports"
case performanceSchema = "Performance Schema Setup"
var id: String { rawValue }
var icon: String {
switch self {
case .serverStatus: return "server.rack"
case .clientConnections: return "person.2"
case .users: return "person.crop.circle.badge.checkmark"
case .variables: return "slider.horizontal.3"
case .dashboard: return "chart.xyaxis.line"
case .performanceReports: return "chart.bar.xaxis"
case .performanceSchema: return "gauge.with.dots.needle.67percent"
}
}
var statement: String {
switch self {
case .serverStatus:
return """
SHOW GLOBAL STATUS WHERE Variable_name IN (
'Uptime',
'Threads_connected',
'Threads_running',
'Questions',
'Queries',
'Connections',
'Bytes_received',
'Bytes_sent'
)
"""
case .clientConnections:
return "SHOW FULL PROCESSLIST"
case .users:
return """
SELECT User, Host, plugin, account_locked
FROM mysql.user
ORDER BY User, Host
"""
case .variables:
return "SHOW GLOBAL VARIABLES"
case .dashboard:
return """
SHOW GLOBAL STATUS WHERE Variable_name IN (
'Uptime',
'Connections',
'Threads_connected',
'Threads_running',
'Questions',
'Queries',
'Bytes_received',
'Bytes_sent',
'Innodb_buffer_pool_reads',
'Innodb_buffer_pool_read_requests',
'Slow_queries'
)
"""
case .performanceReports:
return """
SELECT *
FROM sys.statement_analysis
LIMIT 100
"""
case .performanceSchema:
return """
SELECT NAME, ENABLED, TIMED
FROM performance_schema.setup_instruments
LIMIT 250
"""
}
}
}
struct WorkbenchAdministrationView: View {
let page: WorkbenchAdminPage
let profile: ConnectionProfile?
let close: () -> Void
@State private var result = QueryResult()
@State private var loading = false
@State private var errorMessage: String?
@State private var lastUpdated: Date?
var body: some View {
VStack(spacing: 0) {
header
Divider()
if loading && result.columns.isEmpty {
Spacer()
ProgressView("Loading \(page.rawValue)…")
Spacer()
} else if let errorMessage {
ContentUnavailableView {
Label(
"Unable to Load \(page.rawValue)",
systemImage: "exclamationmark.triangle"
)
} description: {
Text(errorMessage)
} actions: {
Button("Retry") {
Task { await refresh() }
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else if result.columns.isEmpty {
ContentUnavailableView(
"No Data",
systemImage: "tablecells",
description: Text(
"The server did not return any columns."
)
)
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else {
resultContent
}
Divider()
footer
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.task(id: "\(page.id)-\(profile?.id.uuidString ?? "")") {
await refresh()
}
}
private var header: some View {
HStack(spacing: 14) {
Image(systemName: page.icon)
.font(.system(size: 27))
.foregroundStyle(.blue)
.frame(width: 40)
VStack(alignment: .leading, spacing: 3) {
Text(profile?.name ?? "No Connection")
.font(.caption)
.foregroundStyle(.secondary)
Text(page.rawValue)
.font(.system(size: 22, weight: .semibold))
}
Spacer()
Button {
Task { await refresh() }
} label: {
Label("Refresh", systemImage: "arrow.clockwise")
}
.disabled(loading)
Button {
close()
} label: {
Label("SQL Editor", systemImage: "chevron.left.forwardslash.chevron.right")
}
}
.padding(18)
}
@ViewBuilder
private var resultContent: some View {
if page == .dashboard || page == .serverStatus {
ScrollView {
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])
.font(
.system(
size: 23,
weight: .semibold,
design: .rounded
)
)
.lineLimit(1)
.minimumScaleFactor(0.65)
}
.frame(
maxWidth: .infinity,
minHeight: 90,
alignment: .leading
)
.padding(14)
.background(
Color(nsColor: .controlBackgroundColor)
)
.clipShape(
RoundedRectangle(cornerRadius: 10)
)
}
}
}
.padding(18)
}
} else {
NativeResultGrid(
columns: result.columns,
rows: result.rows
)
.frame(
maxWidth: .infinity,
maxHeight: .infinity
)
}
}
private var footer: some View {
HStack {
if let lastUpdated {
Text(
"Updated \(lastUpdated.formatted(date: .omitted, time: .standard))"
)
} else {
Text("Not loaded")
}
Spacer()
Text("\(result.rows.count) rows")
}
.font(.caption)
.foregroundStyle(.secondary)
.padding(.horizontal, 16)
.frame(height: 30)
}
@MainActor
private func refresh() async {
guard let profile else {
errorMessage = "Select a MySQL connection first."
return
}
loading = true
errorMessage = nil
defer {
loading = false
}
do {
let response = try await NativeMySQLRunner.run(
profile: profile,
database: "",
password: PasswordStore.read(profile.id),
sql: page.statement
)
result = response
lastUpdated = Date()
} catch {
errorMessage = error.localizedDescription
}
}
}