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
}
}
}
@@ -7,7 +7,31 @@
"key": "MySQL Workbench",
"location": {
"startingColumn": 18,
"startingLine": 84
"startingLine": 88
}
},
{
"comment": "",
"key": "Navigator",
"location": {
"startingColumn": 20,
"startingLine": 170
}
},
{
"comment": "",
"key": "Administration",
"location": {
"startingColumn": 22,
"startingLine": 171
}
},
{
"comment": "",
"key": "Schemas",
"location": {
"startingColumn": 22,
"startingLine": 172
}
},
{
@@ -15,7 +39,7 @@
"key": "SCHEMAS",
"location": {
"startingColumn": 22,
"startingLine": 169
"startingLine": 192
}
},
{
@@ -23,7 +47,7 @@
"key": "Edit Connection",
"location": {
"startingColumn": 36,
"startingLine": 223
"startingLine": 246
}
},
{
@@ -31,7 +55,7 @@
"key": "Set as Default Schema",
"location": {
"startingColumn": 24,
"startingLine": 280
"startingLine": 363
}
},
{
@@ -39,7 +63,7 @@
"key": "Refresh Tables",
"location": {
"startingColumn": 24,
"startingLine": 284
"startingLine": 367
}
},
{
@@ -47,7 +71,7 @@
"key": "Load Tables",
"location": {
"startingColumn": 28,
"startingLine": 354
"startingLine": 437
}
},
{
@@ -55,7 +79,7 @@
"key": "Tables",
"location": {
"startingColumn": 22,
"startingLine": 327
"startingLine": 410
}
},
{
@@ -63,7 +87,7 @@
"key": "Select First 100 Rows",
"location": {
"startingColumn": 20,
"startingLine": 410
"startingLine": 493
}
},
{
@@ -71,7 +95,7 @@
"key": "New Query Tab",
"location": {
"startingColumn": 19,
"startingLine": 515
"startingLine": 614
}
},
{
@@ -79,7 +103,7 @@
"key": "Default Schema:",
"location": {
"startingColumn": 18,
"startingLine": 540
"startingLine": 639
}
},
{
@@ -87,7 +111,7 @@
"key": "SQL Editor",
"location": {
"startingColumn": 18,
"startingLine": 550
"startingLine": 649
}
},
{
@@ -95,7 +119,7 @@
"key": "Execute SQL",
"location": {
"startingColumn": 19,
"startingLine": 528
"startingLine": 627
}
},
{
@@ -103,7 +127,7 @@
"key": "Format SQL",
"location": {
"startingColumn": 19,
"startingLine": 535
"startingLine": 634
}
},
{
@@ -111,7 +135,7 @@
"key": "%lld",
"location": {
"startingColumn": 26,
"startingLine": 590
"startingLine": 689
}
},
{
@@ -119,7 +143,7 @@
"key": "Result Grid",
"location": {
"startingColumn": 22,
"startingLine": 622
"startingLine": 721
}
},
{
@@ -127,7 +151,7 @@
"key": "No query results",
"location": {
"startingColumn": 26,
"startingLine": 640
"startingLine": 739
}
},
{
@@ -135,7 +159,7 @@
"key": "Action Output",
"location": {
"startingColumn": 22,
"startingLine": 662
"startingLine": 761
}
},
{
@@ -143,7 +167,7 @@
"key": "UTF-8",
"location": {
"startingColumn": 18,
"startingLine": 737
"startingLine": 836
}
},
{
@@ -151,7 +175,7 @@
"key": "Connection Settings",
"location": {
"startingColumn": 18,
"startingLine": 912
"startingLine": 1011
}
},
{
@@ -159,7 +183,7 @@
"key": "Connection Name",
"location": {
"startingColumn": 21,
"startingLine": 917
"startingLine": 1016
}
},
{
@@ -167,7 +191,7 @@
"key": "Hostname",
"location": {
"startingColumn": 21,
"startingLine": 922
"startingLine": 1021
}
},
{
@@ -175,7 +199,7 @@
"key": "Port",
"location": {
"startingColumn": 21,
"startingLine": 927
"startingLine": 1026
}
},
{
@@ -183,7 +207,7 @@
"key": "Username",
"location": {
"startingColumn": 21,
"startingLine": 933
"startingLine": 1032
}
},
{
@@ -191,7 +215,7 @@
"key": "Password (macOS Keychain)",
"location": {
"startingColumn": 21,
"startingLine": 938
"startingLine": 1037
}
},
{
@@ -199,7 +223,7 @@
"key": "Default Schema",
"location": {
"startingColumn": 21,
"startingLine": 943
"startingLine": 1042
}
},
{
@@ -207,7 +231,7 @@
"key": "Cancel",
"location": {
"startingColumn": 24,
"startingLine": 951
"startingLine": 1050
}
},
{
@@ -215,7 +239,87 @@
"key": "Save",
"location": {
"startingColumn": 24,
"startingLine": 955
"startingLine": 1054
}
},
{
"comment": "",
"key": "The server did not return any columns.",
"location": {
"startingColumn": 25,
"startingLine": 1376
}
},
{
"comment": "",
"key": "Loading %@…",
"location": {
"startingColumn": 30,
"startingLine": 1354
}
},
{
"comment": "",
"key": "No Data",
"location": {
"startingColumn": 21,
"startingLine": 1373
}
},
{
"comment": "",
"key": "Unable to Load %@",
"location": {
"startingColumn": 25,
"startingLine": 1360
}
},
{
"comment": "",
"key": "Retry",
"location": {
"startingColumn": 28,
"startingLine": 1366
}
},
{
"comment": "",
"key": "Refresh",
"location": {
"startingColumn": 23,
"startingLine": 1415
}
},
{
"comment": "",
"key": "SQL Editor",
"location": {
"startingColumn": 23,
"startingLine": 1422
}
},
{
"comment": "",
"key": "Not loaded",
"location": {
"startingColumn": 22,
"startingLine": 1500
}
},
{
"comment": "",
"key": "Updated %@",
"location": {
"startingColumn": 21,
"startingLine": 1497
}
},
{
"comment": "",
"key": "%lld rows",
"location": {
"startingColumn": 18,
"startingLine": 1505
}
}
]
@@ -3,3 +3,4 @@
/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app
/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app
/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app
/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app
@@ -19,7 +19,7 @@
<dict>
<key>cdhash</key>
<data>
1TJJDc7LhSK1AHyUbPhKlUy9S8A=
5Wek/oPAL5JjNIHDxN/dkqjd0UI=
</data>
<key>requirement</key>
<string>identifier "WorkbenchReplacement.debug" and anchor apple generic and certificate leaf[subject.CN] = "Apple Development: Mark Gregory (FLBRJ3BYDD)" and certificate 1[field.1.2.840.113635.100.6.2.1] /* exists */</string>
+42
View File
@@ -90,6 +90,48 @@
<key>uniqueIdentifier</key>
<string>8EB6F71B-1109-492D-84B8-8C2FE5C14BB0</string>
</dict>
<key>AD44F407-7FE5-46AE-A82F-8FAC1CA2720B</key>
<dict>
<key>className</key>
<string>IDECommandLineBuildLog</string>
<key>documentTypeString</key>
<string>&lt;nil&gt;</string>
<key>domainType</key>
<string>Xcode.IDEActivityLogDomainType.BuildLog</string>
<key>fileName</key>
<string>AD44F407-7FE5-46AE-A82F-8FAC1CA2720B.xcactivitylog</string>
<key>hasPrimaryLog</key>
<true/>
<key>primaryObservable</key>
<dict>
<key>highLevelStatus</key>
<string>S</string>
<key>totalNumberOfAnalyzerIssues</key>
<integer>0</integer>
<key>totalNumberOfErrors</key>
<integer>0</integer>
<key>totalNumberOfTestFailures</key>
<integer>0</integer>
<key>totalNumberOfWarnings</key>
<integer>0</integer>
</dict>
<key>schemeIdentifier-containerName</key>
<string>WorkbenchReplacement project</string>
<key>schemeIdentifier-schemeName</key>
<string>WorkbenchReplacement</string>
<key>schemeIdentifier-sharedScheme</key>
<integer>1</integer>
<key>signature</key>
<string>Building project WorkbenchReplacement with scheme WorkbenchReplacement and configuration Debug</string>
<key>timeStartedRecording</key>
<real>813875703.86912405</real>
<key>timeStoppedRecording</key>
<real>813875715.47182798</real>
<key>title</key>
<string>Building project WorkbenchReplacement with scheme WorkbenchReplacement and configuration Debug</string>
<key>uniqueIdentifier</key>
<string>AD44F407-7FE5-46AE-A82F-8FAC1CA2720B</string>
</dict>
</dict>
</dict>
</plist>
@@ -1 +1 @@
1792182796.251258: Module build session file for module cache at Path(str: "/Volumes/Developer/Applications/WorkbenchReplacement/build/ModuleCache.noindex")
1792182904.277697: Module build session file for module cache at Path(str: "/Volumes/Developer/Applications/WorkbenchReplacement/build/ModuleCache.noindex")
+1 -1
View File
@@ -3,7 +3,7 @@
<plist version="1.0">
<dict>
<key>LastAccessedDate</key>
<date>2026-10-16T20:33:16Z</date>
<date>2026-10-16T20:35:04Z</date>
<key>WorkspacePath</key>
<string>/Volumes/Developer/Applications/WorkbenchReplacement/WorkbenchReplacement.xcodeproj</string>
</dict>